Golang Sins: The Evil Range Element
Published 2019-6-28What!? How are all 10 items in my array the exact same thing!?!?
The argument that I'm making is that by adopting a simple habit through all of your code, you get the upside of never experiencing a strange language quirk and really no downsides - other than typing an extra line.
Totally worth it.
Instead of ever doing this:
for _, x := range things {
// ...
}
Always do this:
for i := range things {
x := things[i]
// ...
}
If you can stick to that, you'll never have to scratch your head over debug statements that look like this:
Appending "Sprocket"
Appending "Slinky"
Element 0 "Gizmo"
Element 1 "Gizmo"
Problem: Range Elements
Let's say that you're hitting a JSON API
to retrieve a list of widgets and for some reason -
perhaps to filter the list - you make a copy of
the elements while range
ing over them.
What could possibly go wrong?
Let's consider a very simple program that filters a list of Widgets:
package main
import (
"fmt"
"strings"
)
type Widget struct {
Name string
}
func main() {
// As if retrieved from a JSON API
ws := []Widget{
Widget{Name: "Sprocket"},
Widget{Name: "Gizmo"},
}
// Filtering the list as if for a search
search := "S"
results := []*Widget{}
for _, w := range ws {
if strings.HasPrefix(w.Name, search) {
fmt.Println("Added", w.Name)
results = append(results, &w)
}
}
// Let's see what we get!
for _, w := range results {
fmt.Println("Got", w.Name)
}
}
And the result?
Added Sprocket
Got Gizmo
What!?!?
Well, as it turns out, range
re-uses the same memory pointer in the stack
for each iteration of the loop (for efficiency or some such I presume),
so you'll end up with all of your pointers being to the very last item in the array.
This is even easier to observe if we just add another "S" item to our list and re-run our program:
ws := []Widget{
Widget{Name: "Sprocket"},
Widget{Name: "Slinky"},
Widget{Name: "Gizmo"},
}
Now we get this:
Added Sprocket
Added Slinky
Got Gizmo
Got Gizmo
Solution: Only Use Index
Like I said, as a general pattern, just stick to this:
for i := range things {
x := things[i]
// ...
}
Counterpoint:
It is possible that, in very very specific circumstances, you may profile an application that has a very rare loop where the performance of allocating memory changes to a non-trivial degree by using the pre-allocated element. I haven't personally encountered such a case in Go (though I have seen it elsewhere), but I believe it could exist. However, if that happens, you should probably switch to Rust for that part of your application - you'll get bare metal performance control without giving up the appearance and pleasure of a high-level language.
By AJ ONeal
Did I make your day?
Buy me a coffee
(you can learn about the bigger picture I'm working towards on my patreon page )