Fundamentals 15 min read

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.

360 Tech Engineering
360 Tech Engineering
360 Tech Engineering
Common Go Pitfalls: Loop Variable Capture, := Scope, Goroutine Pools, and Struct Memory Alignment

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 0xc000086010

The 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 0xc000096020

Another 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=1

Reordering 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.

ConcurrencyGomemory alignmentgoroutineScopeLoop Variable
360 Tech Engineering
Written by

360 Tech Engineering

Official tech channel of 360, building the most professional technology aggregation platform for the brand.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.