Pointers & Memory
📋 Jump to TakeawaysPointers 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 pointerWhy 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) // 10Without 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) // 31new() 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) // BobSince 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 addressAlways 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) // 42In 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
nilto 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
nilpointer 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