In this post, App. Dev. Manager Vishal Saroopchand showcases similarities and differences on important topics for C# developers learning Go.
In my previous post, I explain my motivation and experience learning Go coming strictly from a C#/JS background. In this post, I want to update the side-by-side comparison with snippets showing similarities and differences on important topics you will encounter in your Go journey.
Setup your Development Environment
Program Structure
// main.go package main import ( "fmt" "os" ) func main(){ // args for i, arg := range os.Args { fmt.Println(fmt.Sprintf("Arg %s at index %v", arg, i)) } fmt.Println("Press ENTER to continue") fmt.Scanln() } func init(){ fmt.Println("Initializer") } Note: Check out C# CommandLine API and Cobra for Go. Both offer enhancements for CLI apps.
Type Declarations
// go package models // Student is a student object type Student struct { ID int FirstName string LastName string Age int } // NewStudent is a constructor function that will return a Student pointer func NewStudent(firstName string, lastName string, age int) *Student { return &Student{ FirstName : firstName, LastName : lastName, Age: age, } }
// c# namespace School.Models { public class Student { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public Student(){} public Student(string firstName, string lastName, int age) { this.FirstName = firstName; this.LastName = lastName; this.Age = age; } } }
Functions
// go // NewStudent is a constructor function that will return a Student pointer func NewStudent(firstName string, lastName string, age int) *Student { return &Student{ ID : 0, FirstName : firstName, LastName : lastName, Age: age, } } // Enroll student into a class func(s *Student) Enroll(c Class) error { return c.Add(s) }
Important differences:
- NewStudent is a constructor function, as such it does not have a receiver (pointer or value).
- Enroll is bound to a pointer receiver *Student
- Add is bound to a value receiver on struct Class
- No access modifiers in Go, uppercase denotes public and lowercase private (only accessible within type or package (unbound))
C#
Methods in C# has the following structure. See the official docs for more information.
AccessModifier unsafe/static/virtual/override/async ReturnType MethodName(Type param1, Type out param2, ref Type param3, params int[] param4) { //Method Body
See C# Language specs on keywords:
- Access Modifiers
- Static Classes and Static Class Members
- Inheritance
- Abstract and Sealed Classes and Class Members
- params
- return
- out
- ref
- tuple return type
- Passing Parameters
Type conversations
// go func exporeTypeConversions() { // use strconv to parse string to int64 i, _ := strconv.ParseInt("-42", 10, 64) // explict conversion of int to float f := float32(i) // convert float to string s := fmt.Sprintf("%f", f) fmt.Println(fmt.Sprintf("i = %v, f = %f, s = %s", i, f, s)) // use i.(type) with switch statement, or string format %T testType := func(i interface{}) string { switch i.(type) { case int: return "int64" case string: return "string" default: return fmt.Sprintf("%T", i) } } fmt.Println(testType(i)) fmt.Println(testType(s)) fmt.Println(testType(models.NewStudent("", "", 18))) }
// c#
static void Main(string[] args)
{
int a = 64;
// implicit
long b = a;
// explicit
int c = (int) b;
// use (Type is Target TempReceiver)
if( c is int d){
//
}
// use typeof(T)
Console.WriteLine(typeof(List<string>));
}
Error Handling
Go lacks C# formal pattern (try, catch, finally) for error handling. The best practice for go is to return a error type as the last return value which the caller can test for an error condition.
s := models.Student{ FirstName: "Jake", } c := models.Class{ Name: "Computer Science", } err := s.Enroll(c) if err != nil { log.Fatalf("Error enrolling student %s in class %s", s.FirstName, c.Name) } //otherwise, continue...
Interface
// go type DataRepository interface { Add(s *Student) (error) Update(s *Student) (error) Get(id int) (*Student, error) Delete(id int) (error) }
// c# public interface IDataRepository<T> { void Add(T s); void Update(T s); T Get(int id); void Delete(int id); }
Notes:
- C# best practice is to use Generics to define interface templates.
- Due to the lack of a formal exception handling (try, catch, finally), we must return an error in Go for error conditions.
- Avoid using “I” as prefix in your Go interface names.
- Go is duck typing while C# requires explicit implementation of the interface.
Inheritance
// go type DataReaderWriter interface { io.Reader io.Writer } type DataWriterReaderImpl struct { } // implements io.Reader func (dwr DataWriterReaderImpl) Read(p []byte) (n int, err error) { // return 0, nil } // implements io.Writer func (dwr DataWriterReaderImpl) Write(p []byte) (n int, err error) { // return 0, nil }
Sample use of DataWriterReaderImpl in Go
buffer := new(bytes.Buffer) json.NewEncoder(buffer).Encode(models.Student{}) dwr := models.DataWriterReaderImpl{} // you can cast to an io.Writer, for example, pass to func by interface type test := io.Writer(dwr) // call write _, err := test.Write(buffer.Bytes()) if err != nil { log.Fatal("Error invoking writer") }
Here is our C# implementation to show the similarities and differences for an similar type.
public interface IWriter { public int Write(byte[] p); } public interface IReader { public byte[] Read(); } public interface IDataReaderWriter : IWriter, IReader { } public class DataReaderWriterImpl : IDataReaderWriter { public int Write(byte[] p){ // return 0; } public byte[] Read(){ return System.Text.Encoding.UTF8.GetBytes("Hello"); } }
Notes:
- C# requires explicit implementation while Go is implicit (duck typing)
Concurrency
// go package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup wg.Add(2) go func() { // simulate work time.Sleep(10 * time.Second) fmt.Println("Job1 completed") wg.Done() }() go func() { // simulate work time.Sleep(5 * time.Second) fmt.Println("Job2 completed") wg.Done() }() wg.Wait() fmt.Println("Done") }
For comparison, here is our C# sample using System.Threading to dispatch new threads.
// old way of dispatching threads var t1 = new Thread(() => { Thread.Sleep(5); Console.WriteLine("Job1 completed"); }); var t2 = new Thread(() => { Thread.Sleep(5); Console.WriteLine("Job2 completed"); }); t1.Start(); t2.Start(); // new & preferred approach for concurrency var t3 = new Task(() => { Thread.Sleep(5); Console.WriteLine("Job3 completed"); }); t3.Start(); var t4 = Task.Run(() => { Thread.Sleep(5); Console.WriteLine("Job4 completed"); }); Task.WaitAll(t3, t4);
Notes:
- Use go-routines (go func()) to start new threads in Go.
- Go-routines don’t bock, you just use sync.WaitGroup or channels to signal.
- C# uses System.Threading or System.Threading.Task to dispatch new threads.
- Use CancellationToken to cancel/signal, see docs.
There is much more to compare, but I hope this gives you a taste of Go as you start your own journey with the language.
References:
Like to see a comparison Dart and C#.
“Go can return more than one values. C# uses out and refs to accomplish this.” – not true about C#. Now (since v.7, IIRC) you can use python-like constructs a la
public (string name, long size, string md5, DateTime mTimeUTC) Get(string path) { … }
Just my 2c.
You are correct, tuples return type was introduced in v7; however you are returning a Tuple. Its no different than creating a wrapper for multiple values hence why it was omitted.
thanks for the post! some notes from an experienced go developer:
<code> use is discouraged, it is difficult to reason about, test and maintain. additionally, you have no guarantee of the order that init functions run in different packages.
avoid constructor functions that do not contain behavior:
<code>
Rather, only use constructors if you need to handle things like error checking dependencies, instantiating slices or maps, or calling out to dependencies. For this example, the calling...
I'm a bit surprised by your "avoid constructor functions that do not contain behavior" advice. What if you later add a behavior, do you have to rewrite all the places that manually instantiate a Student object?
It's a bit like the C# best practice of using properties for any public visible field. It's not that you need a getter and a setter, but rather to ensure that you add them without any impact on the code...
All great points: however, the idea here is to show how they are similar and different.
For example, constructor function – understand for this example its not necessary, but the point is to show you can have a constructor function.
Regarding casting to io.writer, again the point is to show you can downcast.
Hello world example in Go is not quite the same with C#, don’t you think? 😉
Totally agree! I have updated the post, not necessary to compare program structure as C# will be vastly different for project types (WebAPI, Console, Class Libs, etc.).