Be Careful with Pointers in Go's range (A Personal Reminder)
Introduction
Recently, while developing a Go server, I encountered unexpected behavior.
After investigating, I found that the issue was related to how I was using pointers inside a range
. In this post, I want to document the problem and its solution as a reminder to myself.
Problematic Code
The code I was working on was quite different, but to illustrate the issue, I’ve prepared the following example. In this code, a slice called userPtrs
is populated with pointers to elements of users
within a range
.
package main
import "fmt"
type User struct {
Name string
}
func main() {
users := []User{
{"Alice"},
{"Bob"},
{"Charlie"},
}
var userPtrs []*User
for _, user := range users {
userPtrs = append(userPtrs, &user)
}
for _, u := range userPtrs {
fmt.Println(u.Name)
}
}
At first glance, this code seems correct, but when you run it, the output is as follows. All elements point to the last element of users
.
Charlie
Charlie
Charlie
Cause and Solution
The root of this problem is that the user
variable used in the range
loop is being overwritten in each iteration. Although it should be obvious, the range
loop defines a user
variable, which is simply reassigned in each iteration to the next element. In other words, the pointer to the user
variable remains the same throughout the range
.
As a result, all elements in the userPtrs
slice end up pointing to the same memory address, which is the state of the user
variable after the last iteration.
To avoid this, you can create a new variable that directly references the element in users
using its index.
for i := range users {
user := users[i]
userPtrs = append(userPtrs, &user)
}
This produced the expected result.
Conclusion
A similar topic is the use of anonymous functions in a range
loop when dealing with goroutines, but there are likely other pitfalls related to range
.
To prevent such issues, it’s essential to write tests for any areas that seem concerning. In this case, tests saved me once again.