Pointers & Memory
📋 Jump to Takeaways🎁 Go has pointers but no pointer arithmetic — giving you the safety of Java with the memory control of C. How does Go pull off that balancing act?
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 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.
This example shows the mechanics, but in practice passing a pointer to a primitive like int is not a good pattern. An int is 8 bytes — cheaper to copy than to allocate on the heap and pay for pointer indirection. Pointers make sense for large structs where copying is expensive, or when you need to express "this might be absent" with nil. For small primitives, just return the new value:
// idiomatic for primitives
func double(n int) int {
return n * 2
}
x := 5
x = double(x)
fmt.Println(x) // 10Pointer 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 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"
fmt.Println(*p) // 42
fmt.Println(*s) // helloBefore 1.26 you needed a temporary variable: n := 42; p := &n. The new form is shorter and avoids the extra variable.
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
🎁 Your code works but everything lives in one file. How does Go organize code across packages without the chaos of relative imports or circular dependencies?