Nuances matter, but fuck it, this is the internet

This post could alternatively be named a skeptical defense of Go. Skeptical, because, despite having used Go a lot and continuingly chosing Go — I’m not actually a big fan of the language. But at the same time I feel like this subject comes up often enough that I want to write down my thoughts on the language regardless of whether you’re a Go fan or one of the people who insists on jumping into anything about Go and telling everyone how stupid the language is. You do you, but in case you’re actually curious about my thoughts, well, as of 2024 this post tries to explain where I stand.

The entire generics debacle was a good case for something I dislike about the Go language. People in the community were arguing that generics were unnecessary yet libraries made heavy use of reflection and Go had implemented generics specifically for its map, array, and slice data structures. Reflection is neat, but relying on it for type checks turns Go into a Pythonesque zone of compile time type validation that sort of reduce the point of the compiler. Of course you need generics when the reason why you don’t is “there’s this messy, slow, and unsafe hack”.

Go style enums are inherently unsafe at compile time, and yet, because they’re practical that doesn’t bother people enough to fix it.

Go somehow manage to have a combination of all the mess that null brings and a problem with default values at the same time. And that kinda sucks. It’s also an irreversible design decision that we just have to live with. Object-relational mappers will just remain broken and bug prone.

And yet, I often choose Go when given a choice. Because nuances matter. And for me, Go has turned out to be the language where I’m the most productive. There are a few reasons why, and most of them aren’t really properties of the language itself.

Reason 1: Go’s crypto libraries

The primary reason I use Go is because of the cryptography libraries. Having professional maintainers of cryptographical libraries that are written natively in the language and exposed in a user-friendly and secure-by-default manner is… very rare. I once heard a joke that it’d make sense if it turned out that NSA designed Java’s cryptography libraries with the intention of making it easy to make mistakes. In practice, the legacy of OpenSSL and Bouncy Castle seem to be that they were made at a time when people using cryptography were expected to care about cryptography. These days, cryptography is ubiquous and most developers using it will just stay with the defaults.

Everything else about Go I could do without, but having access to good cryptographic libraries that do the right thing by default more often than not is immensely valuable.

Reason 2: Single-binary distribution

I happened to use Python the most during the between-time when Python 3 was released and Python 2 remained more popular. When I wanted to write something that was available for others to use, a lot of care and testing had to go into making sure that it was compatible with your system, because you were “in charge” of the compiler. While I could’ve said “Python 2 only” or “Python 3 only”, I’d rather not have to teach users how to use another project before they can utilize mine.

Go’s statically linked single-binary distribution model is helpful to me in that sense. It’s also one of the biggest gripes I have with Go, since the binary sizes are very large for say a small CLI applications.

The system native binary-ness of it is also rather important to me, comparing it to Java where the start time for the runtime environment prevents me from using it for CLI applications.

Reason 3: Built-in test primitives

Testing is an area where Go has had an approach of providing the essential primitives to make it work and relied on the community to extend it with libraries to have it feature rich. And go test seems to mostly just work to me.

As a bonus point, Fuzzing was added as a core feature to the Go toolchain recently, and I absolutely love it.

Reason 4: Common legibility

This is a very subjective point, but something I’ve experienced is that the Go community tends to try to write code in a fairly common way. go fmt enacting a common standardized formatting is part of that, but I think that it’s equally true that because the language is somewhat lacking in features it doesn’t necessarily attract people trying to outsmart the reader.

The pkg.go.dev common API documentation browsing utility helps, as finding out what is exposed by libraries work the same way for every library and I can quickly jump from the API documentation to the code and vice versa because of the common format.

Even more subjectively, I think this is somewhat approachable, because the Go community isn’t as impressed with naming things based on design patterns as the Java community is or the Microsoftian Hungarian notation. Beyond t, err, and ctx, people aren’t very likely to spend too much time wondering whether something should be called a BeanFactoryBuilder or ServiceCreator or whether a variable should be called usName or inputName. I guess this is a matter of taste, at a previous job of mine I’ve seen a somewhat carefully structured Java library I developed and maintained being rewritten into what I considered a mess over my vacation. It seems fair to assume that the person who rewrote it considered my design to be an unstructured mess. I regrettably didn’t ask.

Go also has figured out a neat way to counter some of the function naming woes plaguing other languages: Just always include the package name: http.Server and ssh.Server both are just “Server” and would have to use an extended name in most languages to add the protocol. In Go, since the package name is considered to be a part of the name (outside of the package itself), Server is a perfectly fine name for an HTTP server living in the http package. That also makes navigating imports a little bit less messy, just stay away from import . "package" (the equivalent to Python’s import * from package).

To me Go’s simplicity of design and community seem to gravitate around a particular style of legibility that sits right with me. Even before I started developing Go, I found libraries such as Javalin and Flask appealing, and I still quite do. The Springs and Djangos of the world are on the wrong abstraction level for my taste.

Reason 5: The compiler is fast

I’m impatient. Go has prioritized compiler performance since day one, and feels a lot like using Python but written with compile-time type safety in mind since the very beginning and not added after twenty years as type hints.

In my opinion, Go sort of bridges the gap between the sluggish compilers and unreliable interpreters, and does so really well.

Some notes on other languages

I’ve already mentioned some other languages I’ve worked with and why I prefer to choose Go over them.

Computer languages, much like human languages, evolve over time. Java, for example, have lost some points in my book since I last used it for work thanks to Oracle’s business practices and gained some thanks to GraalVM bridging some of the concerns I’ve had with using Java for the command-line. If you promise not to tell anyone, I actually prefer Java (8+) syntactically to Go. I keep half an eye on Swift and Rust, but Rust’s cryptographic libraries always seemed like a mess to me (RustCrypto seems to be a somewhat successful attempt to steer that up, maybe I’ll have to try Rust again!) and Swift’s (Linux/backend development) tooling always seemed a bit slow and buggy to me. And since I don’t know them yet, well, there’s a little bit of a dilemma where I can’t be productive in a language that I don’t know and I can’t learn a language without doing something productive in it.

Python is now firmly on Python 3, and type hinting has landed. The tooling around Python seem to really have improved recently, in particular the Rust-based tooling developed by astral.sh (Ruff, UV) seem quite great. I have some work reasons to play around with it now, so that is exciting.

And then we have Erlang. I have a very soft spot for Erlang since my university days. Here’s an example borrowed from erlang.org that sort of highlights some of the things I really like about Erlang’s syntax:

print_list(Stream, [H|T]) ->
	io:format(Stream, "~p~n", [H]),
	print_list(Stream, T);
print_list(Stream, []) -> true.

this is an iteration over a list printing an item to a stream. In Go, this would look something like

func PrintList(stream io.Writer, list []T) bool {
	for _, item := range list {
			fmt.Fprintf(stream, "%v\n", item)
	}
	return true
}

A somewhat fast listing of Erlang language features are: tail-recursion, immutable variables, pattern matching, and no global variables. But that’s not what I really like about Erlang. What I really like is it’s use of ,, ; and . in a way that feels more linguistic than the mathematical based () or {} in other languages:

Expressions are separated by a comma. Blocks are separated by a semicolon. Function declarations, import blocks, et c. end with a period.

With both me and Erlang coming from Sweden, it might not be surprising that I find that this more naturally lends itself to my keyboard layout. It also feels more like prose to me than the mathematically inspired C-syntax, without removing the rigor.

I won’t risk Python’s indentation problems when copying code between files but it (at least to me) feels like it solves the same abundance of brackets and parentheses that seem to help drive Python and YAML adoption. I’ll have to acknowledge that it seems like this is a fairly uncommon opinion of Erlang, and most people I’ve met have described its syntax as odd or off-putting. This might be a part of why Elixir has seen an adoption for borrowing Erlang’s ecosystem but exposing it in a much more common Ruby-esque syntax.

But at the end of it all, here I am, using Go. And reluctantly, I like it. And reluctantly, I wanted to write a post defending it.

Go is actually pretty cool.