Dev Skills

Pointers

What & and * do, when you need them, and why Go pointers cannot hurt you the way C ones can.

Topic 14 of 98 · 5 min to read · 45 min to build
After this topic you can
  • Predict whether a function call changes the caller's value when it takes a value versus a pointer.
  • Write a function that changes its argument through a pointer.
  • Explain how a nil *int tells "no value" apart from zero.

What a pointer is

Every value your program holds lives somewhere in memory, and every location has an address. A pointer is a value that holds one of those addresses instead of the data itself. It is a way of saying “the thing over there” rather than carrying a copy of the thing around.

You need them for two reasons. Go copies values when you pass them to a function, so without a pointer a function cannot change what the caller gave it. And copying a large value on every call costs time you may not want to spend.

Pointers have a bad reputation from C, where they can be moved around arithmetically and freed by hand. Go removes both of those, which takes away almost everything that made them dangerous.

The two operators

pointers.go
x := 42
p := &x // & takes the ADDRESS. p is of type *int.
fmt.Println(p) // 0xc000018030 — an address
fmt.Printf("%p\n", p) // %p is the verb for printing an address
fmt.Println(*p) // 42 — * DEREFERENCES: "the value at this address"
*p = 100 // write through the pointer
fmt.Println(x) // 100 — x changed
var q *int // the zero value of a pointer is nil
fmt.Println(q == nil) // true
// fmt.Println(*q) // PANIC: nil pointer dereference

Why you would want one

Go passes everything by value, so a function receives a copy. A pointer is how you let a function change the caller’s variable.

why.go
func double(n int) { n *= 2 } // changes the copy. Caller sees nothing.
func doubleP(n *int) { *n *= 2 } // changes the caller's variable.
x := 5
double(x)
fmt.Println(x) // 5
doubleP(&x)
fmt.Println(x) // 10
Pointers to structs (`&User{...}`, and why you can write `p.Name` without a `*`) come in **Structs**, once you have met structs.

When to use a pointer

Use a pointerUse a value
the function must change the argumentthe function only reads it
"no value" and zero must be different (see below)zero is a fine default
the value is large and copying is measurablethe value is a number, a bool or a short string

A pointer as "maybe a value"

Because a pointer can be nil, it can say "there is no value here", which a plain int cannot: an int is always some number, even if it is 0. *int lets nil mean "not given" while 0 still means zero.

optional.go
func describe(age *int) string {
if age == nil {
return "age not given"
}
return fmt.Sprint("age ", *age) // could be 0, and that is a real answer
}
This trick matters most for struct fields read from JSON, where it tells "the client did not send this" from "the client sent 0 or an empty string". You will see it in **Structs** and **JSON**.

new versus make

new-make.go
p := new(int) // *int pointing at a zero int. Rarely used.
// make is ONLY for slices, maps, and channels — it initialises them
s := make([]int, 0, 10)
m := make(map[string]int)
ch := make(chan int, 5)
// new(map[string]int) gives you a POINTER TO A NIL MAP. Writing to it panics.
Rule of thumb: `make` for slices, maps and channels, and you will almost never type `new`. For structs you will write `&T{...}`, shown in **Structs**.

What Go pointers cannot do

  • No pointer arithmetic. You cannot do p + 1 to walk through memory, so buffer overruns through a pointer are ruled out.
  • No dangling pointers. Returning &x from a function is safe, because the garbage collector keeps x alive. In C that returns a pointer into a stack frame that no longer exists; in Go, escape analysis moves x to the heap.
  • No manual free. The GC handles it. You cannot double-free or use-after-free.
  • The main thing that can bite you: a nil dereference, which panics rather than corrupting memory. A crash with a stack trace beats silent corruption.
Note: Do not sprinkle pointers everywhere "for performance". A copy of a small value is often faster than a pointer, because it can stay on the stack or in registers, while a pointer can force the value onto the heap where the garbage collector has to track it. Reach for a pointer when you need to mutate or when you have measured a copy cost, not by default.

Try it yourself

press Run — this executes on the Go Playground
package main
import "fmt"
func double(n int) { n *= 2 } // gets a copy
func doubleP(n *int) { *n *= 2 } // gets the address
func describe(age *int) string {
if age == nil {
return "age not given"
}
return fmt.Sprint("age ", *age)
}
func main() {
x := 5
double(x)
fmt.Println("after value call :", x)
doubleP(&x)
fmt.Println("after pointer call:", x)
var missing *int
zero := 0
fmt.Println(describe(missing)) // age not given
fmt.Println(describe(&zero)) // age 0
}
A copy versus a pointer, and nil as "no value".
The short version
  • &x takes an address, *p reads and writes through it.
  • Pointers let a function change the caller’s variable instead of a copy.
  • A nil pointer can mean "no value", which zero cannot.
  • make for slices/maps/channels. No pointer arithmetic, no manual free.
Remember: A pointer is a value holding an address. Go removes the dangerous parts (arithmetic and manual freeing) and keeps the useful part.
Build this before moving on
Write func swap(a, b *int) that swaps two variables, and func maxOf(nums []int) *int that returns nil for an empty slice. Call both and print the results.
Then try these in your own editor
  1. 1Write double(n *int) and call it. Then write doubleVal(n int) and confirm the caller sees nothing.
  2. 2Write safeDiv(a, b int) *int that returns nil when b is 0 and a pointer to a / b otherwise. Print safeDiv(0, 5) and safeDiv(5, 0) and explain the difference.
  3. 3Return &x from a function where x is a local. Confirm it works, then read why with go build -gcflags=-m.
Check yourselfTake the Core language test