Go Tricky: A Bug with For-Range Loops and Pointer
Spent way too much time on a slice and 'for range' problem while working on my API. I thought I knew it all, but turns out, I didn't
As it happens, this problem is a variant of the LoopVar issues. Fortunately, it will be addressed in Go 1.22.
Ever thought you understood something, but then had a problem with it?
That’s what happened to me with Go’s for range
and I spent way too much time fixing an issue with a slice in my API flow.
After I finally solved it, I thought others could learn from my experience and guess what? A teammate ran into the same problem, making me realize I should share my story.
I wanted to filter even numbers from a list, turn them into pointers, and then modify or do something with them.
Here’s the code:
func main() {
pool := []int{1, 2, 3, 4, 5, 6}
even := []*int{}
for _, v := range pool {
if v%2 == 0 {
even = append(even, &v)
}
}
for i := range even {
fmt.Print(*even[i], " ")
}
// .... Doing something complex with even numbers to
// modify them without touching the original pool
}
Want to try guessing what shows up on the screen when you run this?
Spoiler alert, from the title of this article, you can guess it’s not what I thought would happen so it prints 6 6 6
instead of 2 4 6
.
“Why did that happen?”
Well, the for range loop creates a copy of the element in the array when you use a variable like v
so it doesn’t actually use the real element from the array.
The variable v
is initialized once and reused in the loop. Let me recast that for range loop into a standard for loop to clear things up:
for i, v := 0, 0; i < len(pool); i++ {
v = pool[i]
if v%2 == 0 {
even = append(even, &v)
}
}
When you append &v
to the ‘even’ slice, you’re actually storing the address of the v
variable, which changes to 2, 4, and finally 6 in the loop.
So, in the end, all the elements in the even
slice share the same memory address, which is the address of the v
variable (unfortunately).
For instance, when I printed the memory addresses in the ‘even’ slice, they were all the same:
for i := range even {
fmt.Print(even[i], " ")
}
// 0x1400001a228 0x1400001a228 0x1400001a228
And that wraps up my little lesson.
Hopefully, this has been insightful for other developers, including my teammate who stumbled into the same issue and good luck with your coding.