Go čÆčØčµå¼äøēē»ęä½ę·č“é·é±
Source: Julia Evans
Iāve been writing Go pretty casually for years ā the backends for all of my playgrounds (nginx, dns, memory, more DNS) are written in Go, but many of those projects are just a few hundred lines and I donāt come back to those codebases much.
I thought I more or less understood the basics of the language, but this week Iāve been writing a lot more Go than usual while working on some upgrades to Mess with DNS, and ran into a bug that revealed I was missing a very basic concept!
Then I posted about this on Mastodon and someone linked me to this very cool site (and book) called 100 Go Mistakes and How To Avoid Them by Teiva Harsanyi. It just came out in 2022 so itās relatively new.
I decided to read through the site to see what else I was missing, and found a couple of other misconceptions I had about Go. Iāll talk about some of the mistakes that jumped out to me the most, but really the whole 100 Go Mistakes site is great and Iād recommend reading it.
Hereās the initial mistake that started me on this journey:
mistake 1: not understanding that structs are copied on assignment
Letās say we have a struct:
type Thing struct {
Name string
}
and this code:
thing := Thing{"record"}
other_thing := thing
other_thing.Name = "banana"
fmt.Println(thing)
This prints ārecordā and not ābananaā (play.go.dev link), because thing is copied when you
assign it to other_thing.
the problem this caused me: ranges
The bug I spent 2 hours of my life debugging last week was effectively this code (play.go.dev link):
type Thing struct {
Name string
}
func findThing(things []Thing, name string) *Thing {
for _, thing := range things {
if thing.Name == name {
return &thing
}
}
return nil
}
func main() {
things := []Thing{Thing{"record"}, Thing{"banana"}}
thing := findThing(things, "record")
thing.Name = "gramaphone"
fmt.Println(things)
}
This prints out [{record} {banana}] ā because findThing returned a copy, we didnāt change the name in the original array.
This mistake is #30 in 100 Go Mistakes.
I fixed the bug by changing it to something like this (play.go.dev link), which returns a reference to the item in the array weāre looking for instead of a copy.
func findThing(things []Thing, name string) *Thing {
for i := range things {
if things[i].Name == name {
return &things[i]
}
}
return nil
}
why didnāt I realize this?
When I learned that I was mistaken about how assignment worked in Go I was really taken aback, like ā itās such a basic fact about the language works! If I was wrong about that then what ELSE am I wrong about in Go????
My best guess for what happened is:
- Iāve heard for my whole life that when you define a function, you need to think about whether its arguments are passed by reference or by value
- So Iād thought about this in Go, and I knew that if you pass a struct as a value to a function, it gets copied ā if you want to pass a reference then you have to pass a pointer
- But somehow it never occurred to me that you need to think about the same
thing for assignments, perhaps because in most of the other languages I
use (Python, JS, Java) I think everything is a reference anyway. Except for
in Rust, where you do have values that you make copies of but I think most of the time I had to run
.clone()explicitly. (though apparently structs will be automatically copied on assignment if the struct implements theCopytrait) - Also obviously I just donāt write that much Go so I guess itās never come up.
mistake 2: side effects appending slices (#25)
When you subset a slice with x[2:3], the original slice and the sub-slice
share the same backing array, so if you append to the new slice, it can
unintentionally change the old slice:
For example, this code prints [1 2 3 555 5] (code on play.go.dev)
x := []int{1, 2, 3, 4, 5}
y := x[2:3]
y = append(y, 555)
fmt.Println(x)
I donāt think this has ever actually happened to me, but itās alarming and Iām very happy to know about it.
Apparently you can avoid this problem by changing y := x[2:3] to y := x[2:3:3], which restricts the new sliceās capacity so that appending to it
will re-allocate a new slice. Hereās some code on play.go.dev that does that.
mistake 3: not understanding the different types of method receivers (#42)
This one isnāt a āmistakeā exactly, but itās been a source of confusion for me and itās pretty simple so Iām glad to have it cleared up.
In Go you can declare methods in 2 different ways:
func (t Thing) Function()(a āvalue receiverā)func (t *Thing) Function()(a āpointer receiverā)
My understanding now is that basically:
- If you want the method to mutate the struct
t, you need a pointer receiver. - If you want to make sure the method doesnāt mutate the struct
t, use a value receiver.
Explanation #42 has a bunch of other interesting details though. Thereās definitely still something Iām missing about value vs pointer receivers (I got a compile error related to them a couple of times in the last week that I still donāt understand), but hopefully Iāll run into that error again soon and I can figure it out.
more interesting things I noticed
Some more notes from 100 Go Mistakes:
- apparently you can name the outputs of your function (#43), though that can have issues (#44) and Iām not sure I want to
- apparently you can put tests in a different package (#90) to ensure that you only use the packageās public interfaces, which seems really useful
- there are a lots of notes about how to use contexts, channels, goroutines, mutexes, sync.WaitGroup, etc. Iām sure I have something to learn about all of those but today is not the day Iām going to learn them.
Also there are some things that have tripped me up in the past, like:
- forgetting the return statement after replying to an HTTP request (#80)
- not realizing the httptest package exists (#88)
this ā100 common mistakesā format is great
I really appreciated this ā100 common mistakesā format ā it made it really easy for me to skim through the mistakes and very quickly mentally classify them into:
- yep, I know that
- not interested in that one right now
- WOW WAIT I DID NOT KNOW THAT, THAT IS VERY USEFUL!!!!
It looks like ā100 Common Mistakesā is a series of books from Manning and they also have ā100 Java Mistakesā and an upcoming ā100 SQL Server Mistakesā.
Also I enjoyed what Iāve read of Effective Python by Brett Slatkin, which has a similar āhere are a bunch of short Python style tipsā structure where you can quickly skim it and take whatās useful to you. Thereās also Effective C++, Effective Java, and probably more.
some other Go resources
other resources Iāve appreciated:
- Go by example for basic syntax
- go.dev/play
- obviously https://pkg.go.dev for documentation about literally everything
- staticcheck seems like a useful linter ā for example I just started using it to tell me when Iāve forgotten to handle an error
- apparently golangci-lint includes a bunch of different linters