Common Go Pitfalls: Loop Variable Capture, := Scope, Goroutine Pools, and Struct Memory Alignment
This article examines several subtle Go programming issues—including unexpected loop variable addresses, the scope nuances of the := operator, proper handling of goroutine concurrency with worker pools, and how struct field ordering affects memory alignment—providing code examples and practical solutions to avoid these pitfalls.
During Go development engineers often encounter subtle issues that waste time; this article collects four such problems and shows how to solve them.
Loop variable capture : using a range loop and appending the address of the loop variable results in all pointers referring to the same memory location.
package main
import "fmt"
func main() {
in := []int{1, 2, 3}
var out []*int
for _, v := range in {
out = append(out, &v)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
}Running the program prints:
Values: 3 3 3
Addresses: 0xc000086010 0xc000086010 0xc000086010The reason is that the same variable v is reused in each iteration, so all stored pointers point to the same address.
Fix : copy the value to a new variable inside the loop.
package main
import "fmt"
func main() {
in := []int{1, 2, 3}
var out []*int
for _, v := range in {
v := v // copy to a new variable
out = append(out, &v)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
}Now the output is as expected:
Values: 1 2 3
Addresses: 0xc000096010 0xc000096018 0xc000096020Another approach is to take the address of the slice element directly:
for i := range in {
out = append(out, ∈[i])
}:= scope issue : using := inside an if block creates new variables that disappear after the block, leading to unexpected results.
package main
import (
"fmt"
"os"
)
func getUsers() ([]string, error) {
return []string{"小赵", "小钱", "小孙", "小李"}, nil
}
func main() {
var users = make([]string, 0)
envUsers := os.Getenv("USERS")
if envUsers == "" {
fmt.Println("Get users from db")
users, err := getUsers() // creates new users and err
if err != nil {
panic("ERROR!")
}
fmt.Println("Users total: ", len(users))
}
for _, user := range users {
fmt.Println(user)
}
}The variables users and err inside the if are new and are discarded after the block, so the outer users remains empty.
Solution: declare err beforehand and use assignment = inside the block.
var users []string
var err error
if envUsers == "" {
fmt.Println("Get users from db")
users, err = getUsers()
if err != nil {
panic("ERROR!")
}
fmt.Println("Users total: ", len(users))
}Goroutine concurrency : a simple consumer reads from a channel and processes items sequentially.
package main
import (
"fmt"
"sync"
"time"
)
type A struct { id int }
func main() {
start := time.Now()
channel := make(chan A, 100)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for a := range channel {
process(a)
}
}()
for i := 0; i < 100; i++ {
channel <- A{id: i}
}
close(channel)
wg.Wait()
fmt.Printf("Took %s\n", time.Since(start))
}
func process(a A) {
fmt.Printf("Start processing %v\n", a)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Finish processing %v\n", a)
}Spawning a goroutine for each item speeds up processing but can create too many goroutines for large data sets.
go func(a A) {
defer wg.Done()
process(a)
}(a)Best practice is to use a worker pool to limit concurrency.
package main
import (
"fmt"
"sync"
"time"
)
type A struct { id int }
func main() {
start := time.Now()
workerPoolSize := 100
channel := make(chan A, 100)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < workerPoolSize; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for a := range channel {
process(a)
}
}()
}
}()
for i := 0; i < 100000; i++ {
channel <- A{id: i}
}
close(channel)
wg.Wait()
fmt.Printf("Took %s\n", time.Since(start))
}
func process(a A) {
fmt.Printf("Start processing %v\n", a)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Finish processing %v\n", a)
}This limits the number of concurrent goroutines, keeping memory usage under control.
Struct memory alignment : field order influences the size of a struct due to alignment padding.
type BadOrderedUser struct {
IsLocked bool // 1 byte
Name string // 16 byte
ID int32 // 4 byte
}
type OrderedUser struct {
Name string
ID int32
IsLocked bool
}
func main() {
fmt.Printf("BadOrderedUser size: %d\n", unsafe.Sizeof(BadOrderedUser{}))
typ := reflect.TypeOf(BadOrderedUser{})
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
fmt.Printf("%s at offset %v, size=%d, align=%d\n", f.Name, f.Offset, f.Type.Size(), f.Type.Align())
}
fmt.Printf("OrderedUser size: %d\n", unsafe.Sizeof(OrderedUser{}))
typ = reflect.TypeOf(OrderedUser{})
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
fmt.Printf("%s at offset %v, size=%d, align=%d\n", f.Name, f.Offset, f.Type.Size(), f.Type.Align())
}
}Running the program prints:
BadOrderedUser size: 32
IsLocked at offset 0, size=1, align=1
Name at offset 8, size=16, align=8
ID at offset 24, size=4, align=4
OrderedUser size: 24
Name at offset 0, size=16, align=8
ID at offset 16, size=4, align=4
IsLocked at offset 20, size=1, align=1Reordering fields reduces padding and saves memory, which is important for large, frequently accessed structs.
In summary, be aware of loop variable capture, the scope rules of := , control goroutine concurrency with pools, and arrange struct fields to minimise memory waste.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.