Go and C# Comparison

Developer Support

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: