Pointers, deeply explained (no C scars)
If you came from Java or Python, pointers are the one Go feature that feels old-school. Take a deep breath. Go pointers are way friendlier than C pointers. You cannot do arithmetic on them. You cannot accidentally walk off the end of an array. You cannot leak memory by forgetting to free. Go's garbage collector handles all of that.
What you CAN do is decide whether a function gets a copy of your data or a reference to it. That single decision is what pointers exist for.
A pointer is just an address
When you write x := 42, the runtime puts the number 42 somewhere in memory. The variable x is the box. The pointer &x is the address of that box.
x := 42
p := &x // p holds the ADDRESS of x
fmt.Println(*p) // 42 (read the value at that address)
*p = 99 // write through the pointer
fmt.Println(x) // 99 (we just mutated x via p)Two operators:
&x→ "give me the address of x" (take a pointer)*p→ "give me the value at this address" (dereference a pointer)
Mental model: x is a house. &x is the house's street address. *p is opening the door and looking inside.
When to use a pointer
The senior version of this question is: "by value or by reference?"
- Pass by value (no pointer) when the function should NOT mutate the caller's data, and the data is small (under ~100 bytes).
- Pass by pointer when you want the function to mutate the caller's data, OR the data is large and copying it is wasteful, OR the value is "optional" (nil is meaningful).
// Pass by value: pure function, no mutation
func double(n int) int {
return n * 2
}
// Pass by pointer: mutates the caller's data
func incr(n *int) {
*n = *n + 1
}
x := 10
double(x) // returns 20 but x is still 10
incr(&x) // x is now 11Senior rule: pointer receivers for methods that mutate OR when the struct is large. Value receivers when neither. Pick one and stick with it for ALL methods on a type, mixing is a code smell.
new() vs &T
Both create a pointer to a freshly-allocated value. new is rarely seen in modern code.
p := new(int) // pointer to a zero int (value 0)
q := &Customer{} // pointer to a zero Customer struct
r := &Customer{Name: "Aarav"} // with field initUse &T{...} when you want to set fields at creation. Use new(T) only when there's literally nothing to set.
nil and the dereference panic
A pointer that holds no address has the value nil. Dereferencing a nil pointer crashes the program at runtime.
var p *int
fmt.Println(*p) // runtime panic: nil pointer dereferenceAlways check before dereferencing if there's any chance the pointer is nil.
if p != nil {
fmt.Println(*p)
}Interview trap: "What happens if you call a method on a nil receiver?" Answer: it depends. Value-receiver methods can't be called on a nil pointer (you can't dereference nil to copy the value). Pointer-receiver methods CAN be called on a nil pointer, but only if the method itself never reads/writes through the receiver. This is how some patterns (linked-list traversal, optional types) work.
The typed-nil bug (the famous one)
This is the most-asked pointer trap in Go interviews.
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func doWork() error {
var err *MyError // nil *MyError
return err // returns a NON-nil error interface holding a nil *MyError
}
func main() {
if err := doWork(); err != nil {
fmt.Println("error!") // this prints, even though err is "nil"
}
}What is going on? An interface value (error) holds two things internally: a TYPE and a VALUE. Even when the value is nil, if the type is set, the interface itself is NOT nil.
The fix: return nil directly, not a nil pointer of a concrete type.
func doWork() error {
return nil // truly nil interface
}This is THE Go nil trap. Every senior interview hits it.
No pointer arithmetic, ever
In C, you can write p++ and walk through memory. In Go, you cannot. This is a deliberate design choice that eliminates entire classes of buffer overflow bugs. The compiler will refuse.
If you really need raw memory access (you almost never do), there is unsafe.Pointer. The name is a warning, not a feature.
Pointers in practice: when you reach for them
- Mutating method receivers (always pointer if it mutates).
- Large struct passed deep through a call chain (avoid the copy).
- Optional values where nil is meaningful ("user has no manager assigned" →
Manager *User). - Linked data structures (next *Node, parent *Folder).
- Sharing state between goroutines with sync.Mutex protection.
When NOT to reach for them:
- Small structs with up to 5 small fields, just pass by value, clearer and often faster (cache locality).
- Function arguments you don't intend to mutate → value type, the compiler may even prove it doesn't escape.
Pointer vocabulary
- &x
- Take the address of x. Result type is *T where x is T.
- *p
- Dereference p. Returns the value at the address p holds.
- nil pointer
- A pointer variable with no address. Dereferencing it panics at runtime.
- value receiver
- Method defined on T. Gets a copy of the value. Cannot mutate the original.
- pointer receiver
- Method defined on *T. Gets the address. Can mutate the original.
- typed nil
- A nil pointer of a concrete type wrapped in an interface. The interface is NOT == nil. Famous Go bug.
Senior rule: pass value by default. Pass pointer when you must mutate or when the struct is genuinely large. Do not pointer-everything because "it's faster", because for small types it usually is not.
Next lesson: constants and iota, the elegant way Go does enums.
Watching quietly. Tap me if you want a tip.
Go cannot run in the browser. Run opens go.dev/play with your code already loaded (one click, no paste).
Try this (0 of 2 done)
- 1
Predict the output of the program in the editor.
hint
p holds the address of x. *p = 99 writes through the pointer.
show answer
// editor has it - 2
Predict: what does `var p *int; fmt.Println(p)` print?
hint
An uninitialised pointer holds nil.
show answer
var p *int fmt.Println(p)