Go for Dotnet Developers

#programming

It dawned on me with my previous article that it may be a good idea to give an introduction to go for dotnet developers as a way to bridge gaps. Go is a vastly simpler language than C# but it's quirks can be slightly confusing.

Why go instead of rust? The fact of the matter is that go is simply easier to build web services in. While I feel people should give rust a shot, I don't think you should go from something with high level abstractions like dotnet directly to rust. That's a lot to ask and go really is just built for web services.

Modules

Go operates on modules. To create a new module you can use the go mod init {module name} command. You can either give it a short name, making it a local module or a fully qualified name making it reusable. Go modules are delivered via git so it's common to use github. Here's an example "github.com/charmbracelet/bubbletea". If you wish to use a module, reference it in a go file and run go mod tidy or pull it directly with go get {module name} and then reference it.

Packages within modules take up an entire folder. At the top of go files is a reference to the package name. If instead of a module name the package is main, that marks the package as an executable and it's expected you have a main function within the file. This is where one of the quirks of go is. You should only have one go file in a main package. Any modules that aren't in the main package can be referenced from the main package except for other go files in the main package. An example structure.

main.go
models.go
utils /
  json.go

Assuming your main and models file reference the main package you can access the utils package from both but you can't access the models file from the main file without an extra step. In order to do so you need to add both files to build and run commands. go run main.go utils.go would work. You may think that's fine but if you have any sub functions or quite a few files it becomes much more annoying to deal with. The better organization would be this.

main.go
models /
  models.go
utils /
  json.go

That would allow you to reference everything implicitly, even with sub functions or executables. Since models is now a package, you can also organize it better by files.

Visibility is handled at the package level. There is no such thing as internal. There are no keywords at all for privacy. If the thing starts with a capital letter it's usable outside of the package, if it isn't than it isn't. This becomes fairly annoying when dealing with json marshalling but we'll get to that soon.

Object Oriented Programming

Classes do not exist in go.

Inheritance does not exist in go (though embedding is close).

Structs do exist in go, and are the method of grouping fields into objects. Similar to C, structs cannot have instance methods declared at the same time as the struct. Instance methods in go are declared using a syntax similar to how extension methods are declared in C#. An example.

type model struct {
    name string
}

func (m model) printName() {
    fmt.Println(m.name)
}

func (m *model) printName2() {
    fmt.Println(m.name)
}

The accepted struct can be passed by value or a pointer. Keep this in mind if your method is meant to change the original struct. I tend to always use pointers by default for instance methods so I don't catch myself with a large clone. A limitation exists where you cannot extend a struct like this outside of the module in which is was created.

While there is no inheritance there are interfaces. They behave much more like interfaces in TypeScript where you define a shape and any structs that match that shape can automatically be used. You do not need to (and in fact cannot) explicitly declare implementation of an interface.

type printable interface {
	print()
}

type thing struct{}

func (t *thing) print() {
	fmt.Println("Hello")
}

func print(p printable) {
	p.print()
}

func main() {
	print(&thing{})
}

This is a fairly simple example but implicit interfaces make it really easy to extend and mutate functionality using embedding and shadowing. Go doesn't have a built-in inversion of control (IOC) container or any real built-in methods for dependency injection (DI) at all. The solution to that problem is the simple fact that because interfaces are implicit you can swap stuff out as much as you want as long as you use interfaces instead of concrete implementations. While it isn't as abstract as an IOC container, it's very much still testable.

Embedding is a way to combine structs/interfaces and their features. The syntax is simple.

type anotherThing struct {
  thing
}

Doing that will surface any fields or methods on thing on top of anotherThing. A fuller example below.

type printable interface {
	print()
}

type thing struct{}

type anotherThing struct {
	*thing
}

func (t *anotherThing) print() {
	t.thing.print()
	fmt.Println("World")
}

func (t *thing) print() {
	fmt.Println("Hello")
}

func print(p printable) {
	p.print()
}

func main() {
	print(&anotherThing{
		thing: &thing{},
	})
}

This method allows you to shadow instance methods of the original struct with your own implementation, optionally calling the original. This directly violates the open close principle as you really can just replace functionality, but it's extremely useful for extending functionality.

A common tactic is replacing the http.ResponseWriter used by handlers in a middleware to add metadata for logs and metrics or for use in unit testing.

Abstractions

For this section basically all code examples will use this struct.

type Thing struct {
	Name          string
	FavoriteColor string
}

Additionally the examples will use this json.

[
    {
        "name": "Dave",
        "favoriteColor": "Blue"
    },
    {
        "name": "Sally",
        "favoriteColor": "Blue"
    },
    {
        "name": "Jon",
        "favoriteColor": "Red"
    }
]

Serializing and deserializing json can be a little obtuse. Nowadays it's less annoying because of abstractions, but we'll start with the old way without any abstractions.

var result []Thing

_ = json.Unmarshal([]byte(jsonData), &result)

jsonResult, _ := json.Marshal(result)

fmt.Println(string(jsonResult))

Ideally you would actually do something with the error but that code will serialize and deserialize json. If you run that code however you'll notice an issue. Specifically the result will look like this.

[{"Name":"Dave","FavoriteColor":"Blue"},{"Name":"Sally","FavoriteColor":"Blue"},{"Name":"Jon","FavoriteColor":"Red"}]

Astute observers will notice that the casing of the output is incorrect. When you marshal an object it uses the field name by default. This is a problem as the field name needs to be capital due to visibility but the result needs to be cased differently. The solution is a feature called struct tags. These are effectively the same as field level attributes in C#. They provide metadata during reflection. The one to change the serialized (and deserialized) field name is simply json. Here's an updated struct with the appropriate tags.

type Thing struct {
	Name          string `json:"name"`
	FavoriteColor string `json:"favoriteColor"`
}

With the new struct seralization results in the correct field names.

I mentioned that that would be the old way. There is a slight abstraction we can get with generics. Originally go did not have generics, and that was accurate for quite a long time. At this point they've had them for a while which means we can clean up deseralization. Here's a function we can use.

func Deseralize[T any](data string) (T, error) {
	var obj T

	err := json.Unmarshal([]byte(data), &obj)
	if err != nil {
		return obj, err
	}

	return obj, nil
}

Using this we can use it like this.

result, _ := Deseralize[[]Thing](jsonData)

jsonResult, _ := json.Marshal(result)

fmt.Println(string(jsonResult))

It's not terribly cleaner, but personally I prefer it. I normally wouldn't make a serialize method because it's already as basic as I need it to be. Additionally I would normally make the Deseralize method accept a []byte since I'm usually working with a buffer.

A common task in C# is transformation of data through linq methods. There is no equivalent in go. Now that we have generics however these kinds of functions can be created in python style. Here's a reduce function with an example group counter to calculate the number of people with their favorite colors.

func Reduce[TAcc any, T any](list []T, reducer func(TAcc, T) TAcc, init TAcc) TAcc {
	acc := init
	for _, t := range list {
		acc = reducer(acc, t)
	}

	return acc
}

type countMap map[string]int

counter := Reduce(result, func(acc countMap, iter Thing) countMap {
    if val, ok := acc[iter.FavoriteColor]; ok {
        acc[iter.FavoriteColor] = val + 1
    } else {
        acc[iter.FavoriteColor] = 1
    }

    return acc
}, countMap{})

Note that you can alias types to make code cleaner. It also lets you add functions to the new type but that's not needed most of the time. Something to note if you make your own map function. I generally don't use the make function to create stuff since I normally don't know the capacity but a map is the one unique case where you do, so make sure you create a fixed size array when you map an array from one type to another.

If you want a pre-built set of lodash style transformation functions you can look at https://github.com/samber/lo. While it isn't hard to build these things, there's nothing wrong with using libraries. The versions I created for this article can be found here. It's a much smaller surface area, but I'm pretty happy that mine look pretty close to theirs (note: I created mine before I looked up theirs).

HTTP

Building web services is most of what go is used for, so let's go over some gotchas. In the old days we'd use a third party router, kind of like how expressjs works for node. While you certainly still can if you have a complex need for routing like different middlewares on different route groups. Nowadays you can just use the built-in router. I won't go into the basics as there are pretty simple guides, but I will get into the nuance and some recent changes.

If your path ends in a / it acts as a wildcard route. This is most annoying when you have a server-side rendered website. Since if you have something with the / route it becomes a fallback handler for your entire application. https://whynot.sh/caliban for example isn't a route that exists, but is handled automatically. If you don't want this behaviour check the route in the handler and return an appropriate status code.

Before version 1.22 handlers couldn't be filtered by VERB. Now they can if you prefix the route with the verb. Here's my routing section from my website (notice the caliban route doesn't exist).

http.HandleFunc("GET /", state.HomeHandler)
http.HandleFunc("GET /page/{id}", state.PageHandler)
http.HandleFunc("GET /post/{id}", state.PostHandler)
http.Handle("GET /assets/", http.StripPrefix("/assets/", fs))

One last note: the data you get from a request body is a buffer, and the data you send in an HTTP request is a buffer. That means you can feed the body into an HTTP request for efficient-ish request streaming. This makes building BFFs and proxies very easy.

Misc

This here will be a bag of random concepts because I'm bad at organization, but I think these are important enough to make sure they are covered.

Errors

The first topic is related to HTTP in a way, but is more general. That is how errors are handled. In C# you have a stack all the way up to the main function. Throwing an exception will unroll the stack up until it finds a try/catch. When building web services there is generally a fallback global exception handler to deal with any uncaught errors.

Go does not have exceptions. There are two ways errors are handled. The first is a panic. A panic is kind of like an exception but it crashes the program. You do not want panics. While they can be recovered from it's very hard and generally frowned upon. Instead go has errors as values. This means that you have an actual variable that contains the error, and you need to either do something with that error, or pass it up the stack yourself. You will see a lot of this.

if err != nil {
    return nil, err
}

While this can be verbose, it leads to significantly more robust code as you are forced to actually do something with errors. If you are building a web service, once you get up the stack you can build a middleware to consume the error and do something with it. I don't have an example currently for this.

async

Concurrency is the beating heart of web development, and go is no different. Something you will notice is that in go there is nothing to let you know if a function is asynchronous or not. This is done on-purpose. Go functions are uncolored for async. Instead go has a nifty keyword called select that acts as a branched await. There is no special syntax to let anything know you are going to call select, you just do. The branches accept one or more channels. These are primitives that allow for cross thread communication.

In normal async work you create a channel, pass that into a goroutine (what go calls green threads) and call select on the channel (and maybe a timeout channel as well). When the goroutine is complete it should send the response back through the channel.

If you are building web services, it'll be rare that you have to follow that. In general if you are calling a single web service at a time you just do so. The HTTP client will handle everything for you. If you are fanning out requests you use a WaitGroup. Under the hood it creates and selects on a channel for you.

Extras

I am aware having a misc section with an extras section is wack, but again I'm bad at organization.

The last thing I want to mention is the defer keyword. What that does is allow you to call a function after the surrounding function returns. This is used most often to close resources. Similar to how using statements work in C#.

Fin

This isn't an exhaustive list of the differences between go and dotnet. What it is is a starting point. I think people can be wary of learning new languages, and I hope that this article can help to bridge the gap and reduce initial frustration with the language.


Welcome to my website! I like to program.