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

  1. 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

  1. Avoid copying large structs. Passing a 200-byte struct by value copies 200 bytes per call. Passing *T always copies one machine word.

  2. Sharing identity. Two *Person values 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.Pool if 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