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 asjson.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 betweennil
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.