August 27th, 2024 08:20 UTC · 1 month ago

Golang

Relearning Go: Day 12

Bonus: Common Go Mistakes

Today is a bonus day. I’m going to go through an article I found recently, Common Go Mistakes, a distillation of the book 100 Go Mistakes and How to Avoid Them, and write down my thoughts. I’m not going to linger long or comment on everything because I want to get this done today.

Key:

  • ✅ Agree with entry.
  • ❌ Disagree with entry.
  • 🤷 Entry is unnecessary, irrelevant, boring, or I don’t care.
  • Omitted entries are 🤷, i.e. unnecessary, irrelevant, boring, or I don’t care.
  • 😡 Entry makes me sad/angry.

Here goes:

Unintended variable shadowing (#1) ❌

I like shadowing, and it’s useful in Rust, for example. Disagree.

Unnecessary nested code (#2) 🤷

This calls for an imperative style that sometimes makes sense, but sometimes doesn’t. I don’t disagree, but I don’t agree. Unnecessary.

Overusing getters and setters (#4) ✅

Agreed, but this made me laugh (emphasis mine):

Remember that Go is a unique language designed for many characteristics, including simplicity.

any says nothing (#8) ✅

Agreed, but I would go further and say it’s a blight on the language (like it is on TypeScript).

Only use any if you need to accept or return any possible type, such as json.Marshal.

Not everything can be marshalled though. Pass anything! Sometimes get a panic in return!

Not being aware of the possible problems with type embedding (#10) ✅

I agree because type embedding is a weird feature.

Not using the functional options pattern (#11) ✅

A neat pattern, but the example code has a couple of WTFs. A snippet:

type options struct {
  port *int
}
// …
func WithPort(port int) Option {
  return func(options *options) error {
    if port < 0 {
    return errors.New("port should be positive")
  }
// …

WTFs:

  • Since it’s a TCP port, make it a uint16 and then it’s impossible to pass an invalid value.
  • Why a pointer? It’s an int, not the works of Shakespeare.

Neglecting integer overflows (#18) ✅😡

Simple language with behaviour that will silently ruin your day, plus it seems it’s all your problem:

Because integer overflows and underflows are handled silently in Go, you can implement your own functions to catch them.

Not properly checking if a slice is empty (#23) 😡

To check if a slice doesn’t contain any element, check its length. This check works regardless of whether the slice is nil or empty. The same goes for maps. To design unambiguous APIs, you shouldn’t distinguish between nil and empty slices.

Make the mistake of having null in your language, but then magic over it.

Unexpected side effects using slice append (#25) 😡

Copy Your Slices With This One Neat Trick!

Hacks.

Maps and memory leaks (#28) 😡

A map can always grow in memory, but it never shrinks.

Core data structure doesn’t do thing that makes sense for the general use case.

However, maybe this is common in map implementations, and I’m being unfair. Rust’s HashMap doesn’t mention freeing memory, but it does talk about hanging onto memory.

Using defer inside a loop (#35) 😡

defer is ~junk, hence this rule is ~junk because it should not need to exist.

Substring and memory leaks (#41) 😡

Dealing with strings seems fraught. It’s a similar story for slices, but I skipped the earlier entry about that.

Not knowing which type of receiver to use (#42) 😡

Receiver type conflates:

  • Read-only versus read-write.
  • Efficiency of reference versus copy.

This applies to pointer versus value arguments too. Slices, for example, passed by value still refer to the same backing array, so pointer versus value can be even more subtle.

Placeholder for many things about concurrency and parallelism …

…that show that Go is not better suited to these paradigms than many other languages. Go provides elegant building blocks in channels and support for select, but non-trivial applications require many other supporting pieces in addition. Go does not do much in regards to preventing data races and action at a distance; indeed, the append builtin for example can be the source of both. Go’s tooling does have a race detector which is a significant step above the competition – but the language itself missed an opportunity to be intrinsically safe.

Providing a wrong time duration (#75) 😡

Remain cautious with functions accepting a time.Duration. Even though passing an integer is allowed, strive to use the time API to prevent any possible confusion.

As I wrote about on days 7 and 10, the time API is flawed. Its documentation even recommends silly maths around calculating durations. My recommendation is to avoid when possible. Difficult since it’s a core module.

It’s about the cognitive load

Reading through this list I was left with an abiding impression of a language that is touted as simple but actually carries a cognitive load as high as any other language, maybe higher. The apparent simplicity is gained by offloading the cognitive burden onto the developer.

You can get started quickly with Go and feel productive, to discover later that the edge cases and rules around how and when channels and JSON and type embedding and returning from HTTP handlers should be done and remembering to use errgroup and how to calculate durations and avoid data races with append and unintentional copies with range and mutating shared data by mistake and remembering to use go vet and the race detector and being aware of silent integer overflow and knowing when to use slice and string copy tricks to avoid sharing backing data and how to compare values correctly and knowing when to check for nil versus typed nil and how and when defer functions are called and their arguments evaluated and what happens to errors in defer and the verbose error handling idiom which makes composition awkward and nil channels blocking forever and using the right kind of context – these are all on you.