My Opinions on Effective Go


The golden rule of becoming a better writer is reading. To be good at her craft, a writer has to be a reader. I like blogs, I like tech, I like writing about tech, so I should read about tech. After a long time of not having time to write, I wanted to start by summarizing a reading spree that I am on. After finishing the first thing on my reading list Effective Go, I realized that I had some opinions about Go and I want to try to communicate those. I have written my fair share of Go programs, nothing too complicated, nothing too easy. And I have some thoughts about Go. After a long time of focusing on systems languages like Rust and Zig, I now come back to Effective Go. I wanted to read this documentation page for about half a year, I think, and now I have some strong opinions (loosely held, like always) that I want to share with the world.

My Experience With Go

I have written my fair share of Go programs. In my bachelor’s, I’ve written a reverse shell in Go. I have analyzed and reverse engineered Go binaries for my midterm thesis in my bachelor’s. I’ve written some web servers with Go for personal projects. And I have a fairly popular ;) Go program to log out from my Linux system, which I use every day: byebye. My most recent experience with Go was my master’s thesis, where I compared Go and OCaml syntax metrics for maintainability.

So we have established that I have experience with Go and I can have some opinions about the language. So let’s Go!!!

The Good

Go has a lot of conventions. Some people might not like that, but I love it and think that every language should have this level of conventions. If you want to express your feelings, start drawing; don’t write code. Go code always looks the same; the only thing that is different is the actual logic and intent. It should be like that for every language. Take Python, for example: you can write Python and you can write Python. There is an idiomatic way to write Python, but everyone does what she wants.

Effective Go also has very clear recommendations for naming, and things like once.Do() are just great to read and you don’t have to think a lot about how you want to name things. Getters and setters also just make sense and have a good convention: object.Variable() and object.SetVariable() just work.

Go supports labeled breaks, and this is a nice feature to have. It is just convenient and you can do cool stuff with it like in Zig.

Who doesn’t know the feeling when you write the fifth branch of an if-else chain and you think, “I want to have pattern matching in this language, please!” In Go, you can just use a switch without a variable and then basically make it a nice-looking if-else chain, which is not pattern matching, but it is better than nothing. Code now looks like this:

switch {
  case object.IsEven():
    // do something
  case object.IsString():
    // do something
  default:
    // do the default
}

// instead of

if object.IsEven() {
  // do something
} else if object.IsString() {
  // do something
} else {
  // do the default
}

Just some sugar, but I like it.

Garbage-collected language, they said; you don’t have to manage memory, they said; and boom, Go has make and new, and now you have to manage memory. You cannot do everything that you can do with a systems programming language. But you can do more than in JavaScript and Python and I like that. I like that you can benefit from knowing a systems language and knowing how garbage collectors work and use that knowledge to make better Go programs.

Interfaces are just super cool to work with. Any package defines a server interface, and I can just implement this interface for a test struct and for a real struct, and it just works. The power of interfaces is visible in the crypto libraries, where any block cipher just implements a block cipher interface and you can swap out the underlying algorithm at any time with ease.

This is a perfect segue to the very, very good crypto suite that Go provides in the standard library. I recommend listening to the Go Time podcast, in which they talk with the crypto team at Google who work on the crypto library. They do a great job and every language should have its own crypto library because one thing I learned in my cybersecurity bachelor’s is that you don’t roll your own crypto and you don’t trust anyone. Though you have to trust the crypto team that works at Google on the Go crypto library, this is easier than trusting some person somewhere that hosts a Rust package for encryption.

Of course, Go shines with its concurrency features. Channels are just nice to work with; they are a language feature, not an atomic queue like in other languages. The cognitive cost of working with concurrency has been reduced with Go to a very small amount. What I still don’t know is what implications this has. Think of epoll and io_uring and kqueue and OS threads and other concurrency and parallelism mechanisms; they all have a cost. I find it hard to assess the cost of the internal Go concurrency mechanisms. That is one thing that I have to find out at some point.

Panic recovery and error handling is something that polarizes people when talking about Go. I personally like the notion of having errors that have to be handled and that it hurts to handle them because it forces you to think about how you want to handle them. In the case that you don’t want to handle them at all, make a macro and let it write out if err != nil { panic(err) }. Your editor can do that and you don’t have to handle that ever again. I don’t do this because there is a reason that Go does error handling like this. To match the explicit error handling, Go also has recovery. Recently, I worked on a project where I wrote a web client that polled an API, which should never die on me. It was VITAL that this service did not die and could run for days recovering itself. I wrote that in Python, and it was eye-opening because I thought that I had no idea where this thing could error, and I had to wrap everything into one big try-catch so that if it fails I can just restart it. It felt like shit. Some problems could have been handled much more easily than by restarting the program over and over again. Go encourages errors to be propagated upward and libraries not to panic at all. With recover I could have made sure of that and still handled all the little errors gracefully. The project was a lesson. I solved it in Python, but I wish I had done it in Go. I would have felt better. The master class probably would have been Elixir, but I don’t like deploying Elixir to anything because you need the Erlang interpreter, and that is a pain on a Raspberry Pi ;).

The Bad

However, what I dislike in Go is the for loop. The for keyword just does too much for my liking. IMO, it is easier to remember for, while, and maybe loop than to use for for everything with different grammar rules. Remembering the for syntax is probably the only thing in Go that is complicated, except for generics, which were not well received by parts of the community.

Just make a set type in Go, please. The committee opted for generics and just said, well, you can make a weird map with this signature, map[string]struct{}, and then you have a set represented by a map. Yes, you can, but it just looks dumb, and it would be nicer to have a set.intersect() function and functions for other set operations that are built-in and optimized.

There are type conversions in Go. Those are nice to work with when using interfaces, which are a very cool way to add some abstraction to things and make the language very ergonomic, but there is one design decision that trips me up. It is this data := interfaceVariable.(string). A conversion without checking can just blow up in your face. You have to explicitly throw away return values from functions, but for maps and for conversions, you can just choose whether you want to handle that or you want to die, basically. In a language with that many conventions, this is just incomprehensible to me. Maybe this is a historical artifact resulting from the language’s evolution, but I just don’t like it. The language would be better without that extra grammar rule.

The Ugly

First, what sprang out at me and what I hate in every language that does not have the feature is the lack of exhaustive pattern matching or switch statements.

type ByteSize float64

const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

Second, for the love of God, this is a mistake; just have enums. This code makes me think very hard after a long time of not writing Go without any clear benefit. It could just be this:

type ByteSize float64

const (
    KB ByteSize = 1 << (10 * 1)
    MB ByteSize = 1 << (10 * 2)
    GB ByteSize = 1 << (10 * 3)
    TB ByteSize = 1 << (10 * 4)
    PB ByteSize = 1 << (10 * 5)
    EB ByteSize = 1 << (10 * 6)
    ZB ByteSize = 1 << (10 * 7)
    YB ByteSize = 1 << (10 * 8)
)

This is simply clearer for the trade-off of 100 more keystrokes. I don’t see the reason for having a stateful keyword that does something like enums and there are no enums and you have to know what this code does instead of reading what it does. Code is just information encoded in syntax, and when the information is hard to decode that is just bad. There is a reason why people like Zig and Rust and the explicitness of these languages.

Third, … well I guess this is all I dislike about Go. It is a great language.

Conclusion

I thought I had Go down and that there is not that much to learn in Go (after all, it is a pretty lame language), but there were some cool things about Go that I could take away. Effective Go is a really good doc page that has remained relevant even though it is not maintained. The good things definitely outweigh the bad things, and the ugly things you just have to live with. Go is a great language for a huge class of problems. It gives you control where you need it and convenience where you want it. The type system could be better. The switch statements could be exhaustive; damn, they should be. The rest of the language is great. Go is lame, but lame is good; production software does not need to be written in a fancy language.