Pointers & Memory

📋 Jump to Takeaways

Pointers trip people up, but they're simpler than they look. A pointer is just an address. It tells you where a value lives in memory. Understanding pointers is what separates "I can write Go" from "I understand Go." It's also the key to writing efficient code — knowing when to copy and when to share.

What Pointers Are

A pointer holds the memory address of a value. The zero value of a pointer is nil.

x := 42
p := &x          // p holds the address of x
fmt.Println(p)   // 0xc0000b6010 (some memory address)
fmt.Println(*p)  // 42 (value at that address)

& gets the address. * dereferences, reading or writing the value at that address.

*p = 100
fmt.Println(x) // 100 — x changed through the pointer

Why Pointers

Go is pass-by-value. Every function call copies its arguments. Pointers let you avoid copying large structs and let functions modify the original value.

func double(n *int) {
    *n *= 2
}

x := 5
double(&x)
fmt.Println(x) // 10

Without the pointer, double would modify a copy and x stays 5.

Pointer to Struct

Go auto-dereferences struct pointers with dot notation. In C/C++ you'd use -> for pointers, but Go doesn't have that. Just use . for everything.

type User struct {
    Name string
    Age  int
}

u := &User{Name: "Alice", Age: 30}
fmt.Println(u.Name) // Alice — no need for (*u).Name
u.Age = 31
fmt.Println(u.Age) // 31

new() vs &Type{}

Both allocate and return a pointer. &Type{} lets you set fields inline, so it's the preferred way for structs. new() is more useful for basic types like int or bool where you can't write &0.

p1 := new(User)              // all fields zero-valued
p2 := &User{Name: "Bob"}     // Name set, Age is 0

fmt.Println(p1.Name) // "" (zero value)
fmt.Println(p2.Name) // Bob

Since Go 1.26, new() also accepts expressions, not just types. This finally lets you create a pointer to a value in one line:

p := new(42)          // *int pointing to 42
s := new("hello")     // *string pointing to "hello"

nil Pointers

The zero value of any pointer is nil. Dereferencing nil causes a panic.

var p *int
fmt.Println(p) // <nil>
// fmt.Println(*p) // panic: runtime error: invalid memory address

Always check for nil before dereferencing.

if p != nil {
    fmt.Println(*p)
}

Pass by Value

Go always copies. Slices, maps, and channels look like references because they contain internal pointers, but the header itself is still copied. You never need *map or *chan because passing them already shares the underlying data.

func modify(s []int) {
    s[0] = 999 // modifies original — slice header points to same array
}

nums := []int{1, 2, 3}
modify(nums)
fmt.Println(nums) // [999 2 3]

But reassigning the slice itself only affects the local copy:

func reassign(s []int) {
    s = []int{0, 0, 0} // reassigns local copy of header — original unchanged
}

nums := []int{1, 2, 3}
reassign(nums)
fmt.Println(nums) // [1 2 3]

Stack vs Heap

You don't choose stack or heap in Go. The compiler figures it out. If a value stays local to the function, it goes on the stack. If it escapes (returned pointer, closure), it moves to the heap.

func staysLocal() int {
    x := 42     // x lives on the stack, returned as a copy
    return x
}

func escapes() *int {
    x := 42
    return &x   // x moves to the heap because a pointer to it leaves the function
}

p := escapes()
fmt.Println(*p) // 42

In C, returning &x would be a dangling pointer. In Go, the compiler sees the pointer leaving the function and allocates x on the heap automatically. The garbage collector keeps it alive.

When to Use Pointers

Use pointers when:

  • The struct is large and copying is expensive
  • The function needs to modify the original value
  • You need nil to represent "no value"
func updateName(u *User, name string) {
    u.Name = name
}

Don't use pointers for small types like int, bool, or string. The copy is cheap and the code is simpler without indirection.

// unnecessary — just pass by value
func increment(n *int) { *n++ }

// cleaner
func increment(n int) int { return n + 1 }

Key Takeaways

  • & gets a pointer (address), * dereferences it (value at address)
  • Go is always pass-by-value — pointers let you share the original
  • Struct pointers auto-dereference with dot notation
  • new(T) and &T{} both return pointers; &T{} lets you set fields
  • Dereferencing a nil pointer panics — always check
  • Slices, maps, and channels contain internal pointers but are still copied by value
  • Stack vs heap is decided by the compiler — you don't manage it
  • Use pointers for large structs, mutation, and optional values; skip them for small types

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy