38. Pointers: `&` and `*`
A pointer holds the address of a value instead of the value itself.
You've already used them — &Server{...}, pointer receivers, errors.New
returning a pointer. This lesson makes the mechanics explicit.
Go's pointers are deliberately tame: no pointer arithmetic, no dangling pointers, no manual free. What's left is exactly two operations.
& takes an address, * follows one
package main
import "fmt"
func main() {
x := 42
p := &x
fmt.Println("value of x:", x)
fmt.Println("value of p:", p != nil)
fmt.Println("value at p:", *p)
*p = 100
fmt.Println("x is now:", x)
fmt.Printf("type of p: %T\n", p)
}
&x— "the address ofx". The result has type*int.*p— "the value atp". This is called dereferencing.
Assigning through *p changes x, because p points at x. There is one
42 in memory and two ways to reach it.
(We print p != nil rather than p itself because an address is a different
number every run — and there's nothing useful in it.)
Declaring a pointer
package main
import "fmt"
func main() {
var p *int
fmt.Println("zero value:", p == nil)
x := 7
p = &x
fmt.Println(*p)
y := 9
p = &y
fmt.Println(*p, "and x is still", x)
}
*int is the type "pointer to int". Its zero value is nil — pointing at
nothing.
Dereferencing a nil pointer panics, so code that might see one checks first:
if p != nil { ... }. This is the same "check before you use it" discipline
as if err != nil.
Why pointers exist: modifying the caller's value
Remember from module 3 that arguments are copies. A pointer is how a function reaches the original:
package main
import "fmt"
func bumpCopy(n int) {
n += 100
}
func bumpReal(n *int) {
*n += 100
}
func main() {
x := 1
bumpCopy(x)
fmt.Println("after bumpCopy:", x)
bumpReal(&x)
fmt.Println("after bumpReal:", x)
}
Three things had to line up: the parameter type is *int, the call site
passes &x, and the body writes through *n. Miss any one and it doesn't
compile — the mutation is visible at the call site, which is the point.
Whenever you see & at a call site in Go code, read it as "this function can
change my variable."
Structs and the automatic dereference
With structs, Go quietly does the dereferencing for you:
package main
import "fmt"
type User struct {
Name string
Age int
}
func birthday(u *User) {
u.Age++
}
func rename(u *User, name string) {
(*u).Name = name
}
func main() {
u := User{Name: "Ada", Age: 36}
birthday(&u)
rename(&u, "Ada L.")
fmt.Printf("%+v\n", u)
}
u.Age++ on a *User is shorthand for (*u).Age++ — both forms are in that
snippet and they do the same thing. Nobody writes the second. This
convenience is why pointer receivers feel so natural in method bodies.
Returning a pointer is safe
In C, returning the address of a local variable is a bug. In Go it's routine:
package main
import "fmt"
type Config struct {
Host string
Port int
}
func newConfig(host string) *Config {
c := Config{Host: host, Port: 8080}
return &c
}
func main() {
a := newConfig("alpha")
b := newConfig("beta")
b.Port = 9090
fmt.Printf("%+v\n", *a)
fmt.Printf("%+v\n", *b)
}
c is a local variable, and we return its address anyway. Go's compiler
notices the address escapes the function and allocates c on the heap
instead of the stack, where it stays alive as long as anything points at it.
The garbage collector frees it when nothing does.
You never think about this. It's the single biggest quality-of-life difference between Go pointers and C pointers.
Pointers to pointers, and why you won't need them
package main
import "fmt"
func main() {
x := 1
p := &x
pp := &p
fmt.Println(**pp)
**pp = 5
fmt.Println(x)
}
Legal, occasionally necessary (a function that must replace your pointer),
and rare enough that seeing ** in real Go code is a prompt to ask whether
there's a simpler design.
What is not a pointer but behaves like one
This trips people up, so let's be precise. Slices, maps and channels are not pointers — but they contain one:
package main
import "fmt"
func modifySlice(s []int) {
s[0] = 999
}
func appendSlice(s []int) {
s = append(s, 4)
}
func modifyMap(m map[string]int) {
m["new"] = 1
}
func modifyStruct(u User) {
u.Name = "changed"
}
type User struct{ Name string }
func main() {
s := []int{1, 2, 3}
modifySlice(s)
appendSlice(s)
fmt.Println("slice:", s)
m := map[string]int{}
modifyMap(m)
fmt.Println("map:", m)
u := User{Name: "Ada"}
modifyStruct(u)
fmt.Println("struct:", u.Name)
}
Read the results carefully:
modifySliceworked — the slice header was copied, but it points at the same backing array.appendSlicedidn't —appendreassigned the local copy of the header. To change the caller's slice you need*[]int, or (much better) return the new slice.modifyMapworked — maps are reference types all the way.modifyStructdidn't — structs copy, which is why you saw*Userin the last two modules.
The rule underneath is consistent: everything in Go is passed by value. Some of those values happen to contain pointers.
Your turn
Write swap(a, b *int) that exchanges the two values it's pointed at:
before: 1 2
after: 2 1
package main
import "fmt"
// write swap here
func main() {
x, y := 1, 2
fmt.Println("before:", x, y)
swap(&x, &y)
fmt.Println("after:", x, y)
}
package main
import "fmt"
func swap(a, b *int) {
*a, *b = *b, *a
}
func main() {
x, y := 1, 2
fmt.Println("before:", x, y)
swap(&x, &y)
fmt.Println("after:", x, y)
}
Next: when to actually reach for one.