Go and C# Comparison

Premier Developer

Premier

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:

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:

8 comments

Leave a comment

  • Avatar
    plato gonzales

    thanks for the post! some notes from an experienced go developer:

    func init()

    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:

    func NewStudent(firstName string, lastName string, age int) *Student {
    	return &Student{
    		FirstName : firstName,
    		LastName : lastName,
    		Age: age,
    	}
    }

    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 function can directly instantiate the behaviorless *Student and there is one less abstraction layer to think about:

    s := &Student{
    	FirstName : firstName,
    	LastName : lastName,
    	Age: age,
    }
    s.Enroll(c)

    There is usually a more meaningful name for implementations than an Impl suffix, consider having the naming indicate what unique behavior this implementation does, for example:

    type DataReaderWriter interface {
    	io.Reader
    	io.Writer
    }
    
    type NoopDataWriterReader struct {}
    

    Also worth noting that for this particular example, you’re redefining an existing stdlib interface: https://godoc.org/io#ReadWriter .
    Casting to an interface is also an antipattern, usually you should just invoke the interface methods:

    	dwr := models.DataWriterReaderImpl{}
    	//test := io.Writer(dwr)
    	//_, err := test.Write(buffer.Bytes())
    	_, err := dwr.Write(buffer.Bytes())
    
    • Avatar
      Vishal SaroopchandMicrosoft logo

      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.

    • Avatar
      Kevin Gosse

      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 using your library.

  • Alex Shvedov
    Alex Shvedov

    “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.