Advice for new Go Programmers

|
Advice for new Go Programmers

A friend on LinkedIn asked me for pointers for a friend of theirs embarking on a journey to learn Go. Since my list of pointers far exceeded the LinkedIn character limit for a comment and a post. I thought I might as well write it up here instead. Perhaps other might find this useful, too!

This is by no means an exhaustive list. I wrote this list without any real forethought, research, or planning. This is partially because I just wanted to make a quick response to a question, but also because I wanted the ideas that sprung immediately to mind to make up the list. As these were likely going to be the pointers that I’ve given the most thought or attributed the most value to over the years.

1. You will realise Go is enough

As you use Go more and more, you will start to perhaps pine for features other languages have. I wish Go had x, I wish Go had y. But eventually, you will realise you love Go, precisely because it does not have those features.

If you’ve ever worked in a large JS project, a language with every feature imaginable, everyone will use different sets of features, no two areas of the codebase will look the same. When you go from job, to job, to job, that all use Go, and every single codebase more or less looks like the last one.

2. Use concurrency sparingly, but by all means explore it

What drew me to Go, was largely the concurrency model. I wanted to make everything concurrent. But, concurrency adds layers of complexity, it’s harder to reason about, and most of the time the gains simply aren’t worth it. Using concurrency should be the exception, not the rule. Even as easy as Go makes concurrency, it’s still incredibly easy to get wrong. So spend significant time learning concurrency, when and how to exit Go routines most importantly, so that the rare times you do have a good reason to use it, you know precisely how your concurrent approach will behave.

3. Back things up with Benchmarks

A good way to prove the case for concurrency, is to use the built-in benchmark testing in Go. If you can prove your new concurrent data structure, for example, has a significant enough benefit with a benchmark, then you’ve backed up your reasoning. Go provides great tooling for this. Test first, optimise later.

4. It’s all about Context

You will notice context.Context being used everywhere, I spent years passing them around just because everyone else was, not really understanding the power behind it. Especially if you have concurrent operations, then context.Context becomes a powerful tool in managing interactions with external services, I/O bound operations, and much more. See it as a way of communicating signals between multiple paths. Knowing when to timeout a context, when to cancel a context, and how to tidy up a process when a context is cancelled will help you write and manage complex Go projects.

5. Don’t stray from the path

One of the beauties of Go is that there’s typically only one correct way to do or write most things, knowing and following those best practices will get you 90% there in terms of being proficient with the language. Familiarise yourself with the “Effective Go” guidance from the language team: https://go.dev/doc/effective_go

6. Lean on the toolchain

Make sure you’re using an editor that leverages the toolchain, have tools such as $ go vet and $ go fmt running when you save changes, and/or as a part of your CI/CD. The toolchain will do most of the work for you, go vet as part of your editor will nag at you to do things ‘The Go Way’. Which is probably the best way of understanding the languages best practices.

7. Interfaces in Go, the boring game changer

When people think of Go, they think of concurrency, but perhaps the most mind-blowing feature is its interfaces. A construct taken for granted in most languages, but the subtle difference of Go’s interfaces make them one of the most powerful features in the language. It usually takes people a little while to understand why they’re so powerful. Take the io.Reader interface from the standard library:

type Reader interface {
	Read(buf []byte) (n int, err error)
}

This interface isn’t attached to a concrete implementation. Instead, anything that partially matches that function signature, implicitly satisfies that interface. The smaller the interface, the more opportunities. Interfaces with lots of methods in Go, that only satisfy a single implementation of something, are a wasted opportunity. Take the example above, because it only has one simple signature defined, anything with Read([]byte) (int, error) can be a Reader… it might take you some time to truly understand the power of that. But I suggest reading as much as you can about Go’s interfaces, as soon as you can.

8. Don’t overthink paradigms

I see people trying to write ‘functional’ Go, I see people dogmatically trying to write ‘OOP’ Go. My advice, don’t even think about functional vs OOP in Go. Keep it simple, write what makes sense. Good Go programs will use a mix of approaches that look functional-ish in parts, and OOP-ish in other parts. Go’s power lies in its pragmatic versatility.

9. Simplicity over Formality

I’ve seen people from Java backgrounds writing Go, a factory this, an observer that, a mountain of needless abstractions, large interfaces that only satisfy single implementations. The best Go programs I’ve seen or worked with, think more about what makes sense in terms of the simplest solution, rather than reaching for formal design patterns, or dogmatic implementations of the latest architectures, etc, etc. For example, ‘Clean Code’ implementations in Go almost always end up negating the very objective of the language: simplicity. Write the simplest solution first, then only implement abstractions and patterns when they actually help you or other people in reading/extending your code.

Many of the best Go projects I’ve ever seen, have started out as a single main.go file. Don’t be tempted to create a bunch of packages and abstractions right off the bat. Chances are you’ll back yourself into a corner. Wait for patterns to emerge before getting tempted to refactor.

10. No error left behind

Errors in Go are just values. They aren’t treated as anything special. There’s no try/catch mechanism. People new to Go often get sick of repeatedly writing if err != nil {} - but it is there to serve as a reminder that errors are important. They can get lost in a massive try/catch block. Handling errors as values remind the programmer to handle each error with care and consideration. Instead of ‘what do I do when this entire block fails?’ in Go, it’s ‘what do I do when this function call fails?‘. Embrace the terseness of Go’s error handling, and take pride in handling each possible error with care and consideration. Eventually you’ll likely start to think: thank f*ck Go doesn’t have try/catch.

11. A pointer about pointers

You will be exposed to pointers in Go more than you would in many other languages used for web services. In fact, in all my years writing JavaScript, I don’t recall ever thinking about them. Go is slightly lower-level than languages such as Python and JavaScript, which means you are exposed to, and invited to think about how you pass data around a Go program.

Do you pass the reference in memory, or do you pass a copy? I used to find that question a minor inconvenience, but actually, it’s an opportunity to write more efficient code when you truly understand the difference. Don’t ignore that question for too long like I did. You can get by without understanding the difference, but you will get a lot more out of the language knowing the difference and when to use each of them.

Good luck to you, and keep at it.