Fundamentals 13 min read

Why Go Pointers Trip You Up: Common Pitfalls and Deep Dive

This article explains how Go pointers work, why they differ from C pointers, and highlights frequent mistakes such as struct pointer assignments, for‑range loop quirks, closure captures, and memory‑escape scenarios, providing clear code examples and runtime insights.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Why Go Pointers Trip You Up: Common Pitfalls and Deep Dive

Like C, Go has pointers that let you pass a variable's memory address instead of the whole value, which can save memory but also introduces pitfalls that developers often encounter.

1. Pointer‑type variables

In Go you obtain a variable's address with the & operator, creating a pointer that stores the address rather than the actual value. Basic type pointers (e.g., int) cannot be assigned directly; you must dereference with * to read or write the value.

func main() {
    num := 1
    numP := &num
    // numP = 2 // compile error: cannot assign to numP
    *numP = 2
}

Struct pointers behave differently because a struct occupies contiguous memory; accessing testP.Num actually yields a normal variable, allowing direct assignment.

type Test struct { Num int }

func main() {
    test := Test{Num: 1}
    test.Num = 3               // normal assignment
    fmt.Println("v1", test)    // v1 {3}
    testP := &test
    testP.Num = 4               // struct pointer assignment
    fmt.Println("v2", test)    // v2 {4}
}

Slices, maps, and channels are themselves pointer types in Go’s runtime, so you can print their addresses without using &.

func main() {
    nums := []int{1,2,3}
    fmt.Printf("%p
", nums)          // e.g., 0xc0000160c0
    fmt.Printf("%p
", &nums[0])      // same address
    maps := map[string]string{"aa":"bb"}
    fmt.Printf("%p
", maps)          // e.g., 0xc000076180
    ch := make(chan int, 0)
    fmt.Printf("%p
", ch)            // e.g., 0xc00006c060
}

Internally, creating a slice returns a pointer to the first element; creating a map returns a pointer to an hchan structure. The fmt.Printf implementation detects these pointer‑like types via reflection and prints their underlying addresses.

2. Go only has value passing, not reference passing

When a function receives a pointer, the pointer value itself is copied (value passing), but the copy points to the original data, which is why it appears as “reference passing”.

type User struct { Name string; Age int }

func setNameV1(user *User) { user.Name = "test_v1" }
func setNameV2(user User) { user.Name = "test_v2" }

func main() {
    u := User{Name: "init"}
    fmt.Println("init", u) // init {init 0}
    setNameV1(&u)
    fmt.Println("v1", u)    // v1 {test_v1 0}
    setNameV2(u)
    fmt.Println("v2", u)    // v2 {test_v1 0}
}

The same principle applies to slices, maps, and channels.

3. for‑range and pointers

When using for range with a pointer to the loop variable, the same address is reused each iteration, causing all appended pointers to reference the last element.

type User struct { Name string; Age int }

func main() {
    userList := []User{{"aa",1},{"bb",1}}
    var newUser []*User
    for _, u := range userList {
        newUser = append(newUser, &u) // all pointers point to the same u
    }
    for _, nu := range newUser {
        fmt.Printf("%+v
", nu.Name)
    }
}

Printing the address inside the loop shows the address remains constant.

point: 0xc00000c030
val: aa
point: 0xc00000c030
val: bb

A similar issue appears in goroutine closures that capture the loop variable’s address.

func main() {
    for i := 0; i < 10; i++ {
        go func(idx *int) { fmt.Println("go:", *idx) }(&i)
    }
    time.Sleep(5 * time.Second)
}

4. Closures and pointers

A closure that captures a pointer variable will see changes made to that variable outside the closure.

func incr1(x *int) func() {
    return func() { *x = *x + 1; fmt.Printf("incr point x = %d
", *x) }
}
func incr2(x int) func() {
    return func() { x = x + 1; fmt.Printf("incr normal x = %d
", x) }
}

func main() {
    x := 1
    i1 := incr1(&x)
    i2 := incr2(x)
    i1(); i2(); i1(); i2()
    x = 100
    i1(); i2(); i1(); i2()
}

5. Pointers and memory escape

Memory escape occurs when a pointer forces a value onto the heap, increasing GC pressure. Common escape scenarios include returning a pointer to a local variable, storing a pointer to a variable that has already escaped, and placing pointers inside slices, maps, or channels.

type Escape struct { Num1 int; Str1 *string; Slice []int }

func NewEscape() *Escape { return &Escape{} } // &Escape{} escapes to heap

func main() {
    e := &Escape{Num1: 0}
    name := "aa"
    e.Str1 = &name // name moves to heap because e escaped
    arr := make([]*int, 2)
    n := 10
    arr[0] = &n // pointer stored in slice also escapes
}

Understanding these patterns helps you write more efficient Go code.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Runtimepointersclosuresfor-rangememory escape
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

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.