Iain McLaren/Golang's strangeness is awesome

Created Mon, 08 Apr 2024 00:00:00 +0000 Modified Mon, 08 Apr 2024 00:00:00 +0000
2062 Words

Golang’s strangeness is awesome.

Why go (golang) is my favourite programming language.

Introduction (and why not rust?)

Every now and then I am asked why go is my favourite programming language. There are so many reasons, so here is a summary. Like all programming languages that I have used, there are also some issues with go and dark corners of go. To hopefully show that I am not too narrow minded, I list some of these issues and dark corners at the end of this post.

In a different timeline, this could have been an article about rust. Rust seems like a great language, but rust was released in 2015 which was after I first needed to build a tool using a programming language like go or rust. Rust seems to solve similar problems as go, and seems even more pedantic and opinionated than go … which is a compliment. But … well … I’m used to go. Maybe I will change my mind in the future?

In any event, I like my technology as old, boring, and universally available as possible (i.e. available using all the main cloud providers). For example, I like:

  • Debian or Ubuntu linux servers;
  • s3 compatible services for storage;
  • postgresql; and
  • golang, python, and bash scripts.

I don’t want to use tech that is likely to change in a way that breaks anything, or disappear in the near future. Rust is pretty old and boring, but not quite as old as boring as go.

Some reasons why golang is my favourite language

Go is fast

Go became my favourite programming language the second time that I had to switch from python to go for a project because python ran too slowly.

I wanted a language that runs close to the speed of c, but with safeguards against my bad coding such as enforcement of strict typing and memory management.

Go is pretty, simple, and easy to understand

I like programming languages that are simple, graceful, and easy to read and understand.

In my eyes, uglier languages include:

  • c and c++, although they are forgiven because their ugliness (particularly lack of memory management) bring us closer to the machine without requiring us to resort to assembly.
  • c#, java, and objective c. Urgh, ugly, ugly, ugly.

Middling languages include:

  • rust. I like a lot about Rust. My only very minor gripe is that I find code with double colons “::” a bit hard to grok. I will probably get over this given time.
  • python and ruby. Python is my favourite scripting language, but is dangerous at scale. I don’t love code that operates differently depending on tabs and/or whitespace, has implicit error handling, and does not require me to declare types. Too dangerous.

Pretty languages include:

  • SwiftUI - for macOS and iOS apps only. But I like it. Easy to read and pretty.
  • Go (of course) - a very simple language with limited magic. The go developers also seem to resist adding features that encourage magic or that is difficult to read and understand, such as complex inheritance or similar features that change how the standard library works.

Go has a simple but surprisingly broad standard library

Go has a relatively small and simple standard library. The tour of go and go by example will get you a very long way in understanding how go works.

It may be a quirk of history, and because of the networking focus (golang has always very much been a language driven by Google), but you can get a very long way just using the standard libraries.

Newcomers to go are often surprised that many go programmers recommend not using a web framework, for example. Instead, it’s usually best to use standard libraries, and only limited third party libraries where absolutely required. For example, I commonly use thin wrappers over the standard library such as the gorilla framework and sqlx, but mainly just stick to the standard library.

Go has fantastic built in options for error handling and concurrency

goroutines, golang channels, contexts, and the sync standard library (particularly sync/WaitGroup are incredibly powerful and useful tools for concurrent programming.

Go is agressively backwards and forwards compatiabile

I still use go code that I wrote years and years ago. It is one of the few programming language that I have ever used that allows me to recompile really old code without any changes. Try doing that with node shudder. I also like that most third party go libraries also follow this convention.

One caveat is that go has recently introduced go modules which has added some complexity when we update multiple libraries and modules as part of a larger project. However, go modules have brought us simpler and safer version control and semver compliance which is ultimately a good thing. In my experience, if you build with go using a third party go library, and update to the latest version of that third party library years later, you can be almost certain that your old code will still run.

Go has opinionated tools, meaning that most go code is simple and easy to read

If you use VSCode to develop go. And I my view you should. Every time you save a go file, VSCode updates the code to “standardised” spacing. This is a bit of a shock the first time it happens, but makes for very readable code across projects. I like it.

Go also does not have features that would encourage magic or would otherwise be difficult to read and understand. I also like that go runs very fast, but prioritises code readability over raw speed. The go developers even resisted introducing generics for many years. Generics are great, but I haven’t needed to use them. I built one library pre-generics which probably could be simplified by using generics, but I do not use generics in any of my existing code.

Testing is a first class feature of go

Any file ending in “_test.go” can contain test functions that are run with the go test command. This allows for, and pushes developers towards, simple test driven development.

Creating these tests files also exposes if a go file is too long and/or if our project needs refactoring. If all of the functions in a .go file cannot be relatively easily tested using its _test.go file, then we probably need to refactor.

Go has strict code constraints

I like static typing. I think that it makes code much easier to read, and has saved me from bugs more times than I can count.

If you create a go variable (or import a library) and don’t use it, then the go compiler won’t allow you to build the code until you delete the variable or library or update the code to use the variable or library. It sounds annoying but encourages clean coding.

Go has fantastic standard documentation

The golang standard library documentation includes examples for most libraries making them very easy to understand and use.

This is great, particularly because documentation outside the standard library is … not so great.

Go doesn’t let you take shortcuts

For example, to add a new item to a slice, we use the following go code:

s = append(s, newitem)

Why so verbose? Because adding a new item to a slice uses up computing resources in a relatively expensive way. Go refuses to hide this complexity.

Go requires explicit error handing

A common complaint about go is that it is littered with if err != nil lines. For example:

import "fmt"

func main() {
	for i := 0; i < 100; i++ {
		err := anythingButFourtyTwo(i)
		if err != nil {
			panic(err)
		}
	}
}

func anythingButFourtyTwo(i int) error {
	if i == 42 {
		return fmt.Errorf("so long and thanks for all the fish")
	}
	return nil
}

I love this because we need to explicitly handle every error. When you review a new go code base, this makes it much easier to understand how the code works, and critically what happens when there are errors.

Go’s strangeness is awesome

Coming from a c and python background, I found the following both strange and awesome about go:

  • Goroutines, and sharing memory by communicating instead of communicating by sharing memory.
  • Use of interfaces generally, including in the standard library, such as the use of io.Reader and io.Writer.
  • Version control being effectively built in. The go ecosystem’s reliance on github is something to watch long term, but version control is relatively easy and go libraries generally follow the approach of the go standard library by rigorously complying with semver.
  • Layering contexts and error handling throughout large projects.
  • etc.

Some issues with go

Go is easy to learn but difficult to master

Golang has simple and incredibly powerful built in options for concurrency such as goroutines, golang channels, and the sync standard library (particularly sync/WaitGroup. However, it is very easy to create buggy code that:

  • creates thousands or millions of goroutines that use all of the machine’s memory;
  • doesn’t handle errors;
  • deadlocks;
  • attempts to close open channels; or
  • panics when it encounters an unexpected nil variable.

There are best practices coding approaches that we can use to avoid these problems, but learning these best practices only comes with experience.

Dark corners

Like any language, go has some dark corners that are best avoided if possible, including:

  • overuse or unnecessary use of interfaces, reflect, and the unsafe package;
  • building code that may use nil pointers (causing panics);
  • lazy goroutine creation and use, leading to memory leaks and attempting to close open channels;
  • memory leak risks when passing pointers between functions; and
  • lack of control over garbage collection.

During my go journey, so far I have struggled with (in this order, and particularly with relatively large codebases):

  • creating concurrent code that exits and restarts gracefully, and use of contexts;
  • finding and fixing memory leaks; and
  • garbage collection and opaque memory management.

Golang tooling is very good, but sometimes its garbage collection seems a little opaque. It is relatively easy to write go code that uses a lot of memory (or all the memory) and I only learned over time to use pprof effectively to find memory leaks, and how to code to use a very small memory footprint.

Once coded well, go code is incredibly memory efficient. It is usually quick and easy to reach the point where the CPU, or filesystem or network i/o, become the bottlenecks instead of available memory (i.e. the speed of the running go code is no longer the performance choke point).

Limited great books and documentation outside of the standard library

The standard library documentation is fantastic. There are also a lot of go books and articles. However, because golang is not as widely used as python (for example) once we move away from simple uses of the standard library less great documentation exists.

For example, I found that learning to use go’s simple and powerful concurrency features to build complex apps, with multiple error fallbacks, was more challenging than expected. A really great go concurrency at scale cookbook would have saved me a lot of time.

Go articles also often focus on less complex apps that can ingest all data into memory. For example, many golang articles and third party golang libraries read the entire contents of a file into memory by default rather than streaming and then processing individual bytes and/or lines of code in a file. This in spite of the fact that the go standard library provides robust tools and processes to allow for streaming such information (such as io.Reader) as bytes or lines.

My frustration is that go does allow us to easily build complex apps that ingest huge amounts of memory. However, the go books and articles that I have read do not show us the best practices for how to do so. All of the issues discussed above can be managed with best practice coding, but I have not read any books that describe how to write really robust go apps that avoid these issues.

I have learned how to avoid these issues over time by improving my go coding by trial and error. However, it has always been clear to me that any issues that I have with go relate to my lack of understanding of how to code well rather than any underlying problems with the language itself.