Coming to Go from C#: What I had to unlearn

Moving from C# to Go requires more than just learning new syntax—it requires a mindset shift. Explore how a veteran .NET developer tackled Go's approach to OOP composition, explicit error handling, and interface-driven generics.

Introduction

I have been a backend developer for the past 30 or so years, and the last 20 years I spent most of my time writing in C#, mostly for ASP.NET applications. And although I love C# ,a couple of years ago I stumbled upon a language called Go. This language seemed easy to learn and fast. The opinions expressed in the article are entirely my own

The Good

Let’s start with the fact that Go seems simple, on the surface, especially when compared to a language like Rust. This is not say that Rust is bad, I like Rust, and like Go it has its strengths and weaknesses.

With Go I could whip up a simple Web application in minutes. With a little more work I could add more endpoints and a database, all working superfast. You have a choice which API framework to use, either the one provided in the Go standard library, Chi, GIN, Fiber and many others. The same went for the database framework. Those who have read my previous posts know that I am not too big a fan of ORMs, but even if you want an ORM there is GORM for example.

When developing in C# you are used to importing nuget packages to write for example an ASP.NET Web API. This is not a bad thing, but having basic things like http serving and routing in the standard library is quite a nice thing.

OOP Or The Lack Thereof

The main thing I had to unlearn was not having classes and objects. In Go you have structs which can have method attached to them, but there is no such thing as inheritance.

Is that a bad thing? In many cases I’d say no. Many codebases I have worked on had rather convoluted object models, where you had a factory class to build more factories to finally build an object for example.

What Go does well is keep to one of the main rules in OOP namely: favour composition over inheritance. Go allows to embed structs in other structs, and this can be quite a powerful mechanism.

Did I miss using OOP when I started using Go? Yes, it was one of the main things I had to unlearn. However, not having endless inheritance trees, unnecessary factory and builder patterns is quite a relief. Mind you: you can make Go code complicated as well, however, and that is my personal opinion, the language itself almost forces simplicity and readability.

Go interfaces are another thing I had to get used to. In many OOP oriented languages, like C# but also Java, when you want a class to implement an interface you put that in the class declaration, and the compiler will force to implement all the interface’s methods.

Go takes a different route: you don’t declare the interface when declaring the struct, you simply implement the interface’s methods, and if all these methods have been implemented, you have implemented the interface. That is quite a different approach, which took a while to get used to.

For this I am in two minds: knowing which interfaces a class implements can keep the code readable, with Go I am just guessing sometimes, so that is a bit of a complaint about Go.

Error Handling

Error handling, in the many projects I have worked on, was usually not a first concern. Many developers just walk the happy path, not out of laziness or negligence, but sometimes it gets simply forgotten, or there are delivery deadlines and such.

If you have a method in C# which can throw an exception, you can simply ignore it, and let the application crash. It is not something you should do, but something you could do if, for example, you are in a hurry, and I have seen it happen more than once. To be fair, mature C# web applications usually mitigate this by catching unhandled exceptions globally at the middleware level, but the language itself does not force you to handle the exception at the call site.

Languages like Go make error handling explicit. Many functions in the standard library return an error which makes it clear an error could occur and should be handled, or passed on as the case may be.

This way of error handling can lead to lots of repeated boiler plate code, which is a common complaint with Go code. However, explicitly returning an error forces the developer to handle it, which can make the code more robust. Having to deal with errors explicitly can be quite tiring at first, however, it also makes you aware of where code can go wrong, and you are more or less forced to deal with that.

Generics

Generics were a rather late addition to Go, and have been in C# since version 2.0 if I remember correctly. When used well, generics can increase typesafety and readability. A nice feature of both C# and Go is that you can have multiple constraints when declaring generic types. In C# you can do this by using the where keyword for example. In Go you can do that by combining the interface in a combined interface like this:

import "io"

// Create an interface that embeds both io.Reader AND io.Writer
type ReadWriter interface {
    io.Reader
    io.Writer
}

// T must now satisfy both
func Process[T ReadWriter](item T) {
    // You can now call both Read() and Write() on item
}

or you can use anonymous interfaces like this:

import "io"

// T must implement both io.Reader and io.Closer
func ReadAndClose[T interface{ io.Reader; io.Closer }](item T) {
    // ...
}

However, Go goes a bit further than C# by allowing you constrain a generic to a specific list of concrete types using the union operator:

// T can only be an int or a float64
type Number interface {
    int | float64
}

func Add[T Number](a, b T) T {
    return a + b
}

There is even an approximation operator (~) in Go, which allows you to accept any custom type as long as its underlying memory structure matches the constraint. While powerful, these subjects deserve an article of their own, I think.

What is clear, however, is that Go and C# have a different approach to OOP and generics, or rather C# has one, and Go has taken the best bits, like composition and union types, and ran with that. As a C# developer, that takes some getting used to, however, as I mentioned, Go’s approach almost prohibits complicated object trees and the overuse of some design patterns.

Conclusion

As you can see I had to unlearn quite some habits when moving from C# to Go, on the other hand I gained some valuable insights and habits.

C# and Go each have their own strengths and weaknesses, and I would decide on a case-by-case basis which to use.

The Code Nomad
The Code Nomad
Articles: 169

Leave a Reply

Your email address will not be published. Required fields are marked *