Pointer basics
A pointer holds the memory address of a value. The type of a pointer to T
is *T. The zero value of any pointer is nil.
x := 10
p := &x // p has type *int
fmt.Println(*p)// 10 — dereference
*p = 99
fmt.Println(x) // 99
You take an address with &, you dereference with *. You will not see
pointer arithmetic in normal Go (unsafe exists but is rarely needed).
Why pointers exist
- Mutation across function calls. Without a pointer, function arguments are copied:
go
func bump(n int) { n++ } // changes a copy
func bumpP(n *int) { *n++ } // changes the caller's value
-
Avoid copying large structs. Passing a 200-byte struct by value copies 200 bytes per call. Passing
*Talways copies one machine word. -
Sharing identity. Two
*Personvalues can refer to the same underlying object.
Creating values: literals, &T{}, and new
p1 := &Point{1, 2} // common
p2 := new(Point) // returns *Point, fields zeroed
p2.X = 1
p2.Y = 2
Both produce *Point. &T{} is preferred because it lets you set fields
inline. new is useful when you want a pointer to a primitive
(p := new(int) returns *int pointing to 0).
Nil checks
var p *int
if p == nil {
fmt.Println("nil")
}
*p = 1 // PANIC: nil pointer dereference
A nil interface vs a nil pointer wrapped in an interface is a subtle
gotcha; see errors-related examples.
Stack vs heap (and "escape analysis")
Where a value lives in memory is decided by the compiler, not by new vs &.
- If the compiler can prove a value does not outlive its function, it goes on the stack (cheap, freed when the function returns).
- If a value's address escapes the function (returned, stored in a global, passed to a goroutine), it is allocated on the heap and reclaimed by the garbage collector.
You can inspect this with go build -gcflags="-m".
In day-to-day Go you do not manage memory; you write the code that makes sense and trust the compiler + GC. Pointers are about semantics (sharing, mutation) more than about performance.
Pointers to slices, maps, channels
Slices, maps and channels are already reference types — they internally hold
a pointer to a backing structure. You normally don't need *[]int or
*map[string]int. Pass the slice/map directly.
The exception is when a function reslices and you want the caller to see the
new slice header (length / capacity changed in a non-append-then-return
pattern). That's rare; idiomatic code returns the new slice instead:
func filter(s []int, keep func(int) bool) []int {
out := s[:0] // reuse backing array
for _, v := range s {
if keep(v) { out = append(out, v) }
}
return out
}
Memory model in two sentences
The Go memory model says that without synchronization, one goroutine has no
guarantees about the order in which writes from another goroutine become
visible. Use channels, sync.Mutex, sync/atomic, etc., to
establish a happens-before relationship.
You will see this in action in Chapter 10.
Garbage collection
Go uses a concurrent, tri-color, mark-sweep collector. You don't free memory manually. To keep GC pressure low:
- Avoid unnecessary allocations in hot paths (preallocate slices, reuse
buffers via
sync.Poolif profiles show it). - Use value types where reasonable so the GC has less to scan.
- Clear pointer slots in long-lived containers when removing entries
(e.g.,
q.buf[head] = zero) — see the queue implementation in Part 2.
Run the example
go run ./part1-language/examples/08_pointers