Pointers
Reviewed & published by Brayan K
Pointers let a function reach back and change the caller's data instead of working on a copy. Go's pointers are far gentler than C's — no pointer arithmetic, garbage-collected memory — but the two operators & (address-of) and * (dereference) unlock efficient, mutable code. This lesson demystifies them once and for all.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Take a value's address with &
- Follow a pointer with * (dereference)
- Mutate a caller's variable by passing a pointer
- Understand value-copy vs pointer semantics
- Allocate with new and recognise the nil zero value
- Access struct fields through a pointer (auto-deref)
1️⃣ Address-of & Dereference
Two operators do all the work. &x gives you the address of x — a value of type *int if x is an int. *p dereferences the pointer: it follows the address to read or write the value living there. Writing through a pointer changes the original variable — that's the whole point.
package main
import "fmt"
func main() {
// A pointer holds the ADDRESS of a value, not the value itself.
x := 10
p := &x // & = "address of x". p has type *int.
fmt.Println(p == nil) // false — p points at real memory
// * = "dereference": follow the pointer to the value it points at.
fmt.Println(*p) // 10 (the value at that address)
// Writing through the pointer changes the ORIGINAL variable.
*p = 25
fmt.Println(x) // 25 (x changed, via p)
// The pointer itself is just an address; printing it shows hex.
fmt.Printf("%T\n", p) // *int
}2️⃣ Pointers as Function Parameters
This is where pointers earn their keep. Go passes everything by value — a function receives a copy, so editing a plain parameter never touches the caller's variable. Pass a pointer instead and the function can modify the original by dereferencing it. Compare the two functions below: only the pointer version's change survives.
package main
import "fmt"
func main() {
balance := 100
// Passing a VALUE copies it — the function can't change the original.
addByValue(balance, 50)
fmt.Println("after value:", balance) // after value: 100 (unchanged)
// Passing a POINTER lets the function modify the caller's variable.
addByPointer(&balance, 50)
fmt.Println("after pointer:", balance) // after pointer: 150
}
func addByValue(b int, amount int) {
b += amount // edits the local copy only
}
func addByPointer(b *int, amount int) {
*b += amount // edits the value the pointer points at
}3️⃣ new, nil & Structs
The built-in new(T) allocates a zeroed T and returns a *T. Every pointer's zero value is nil, and dereferencing a nil pointer panics — so guard with a check when a pointer might be unset. A lovely convenience: with a pointer to a struct, Go lets you write p.Field and auto-dereferences for you — no need for the clunky (*p).Field.
package main
import "fmt"
type Counter struct {
Hits int
}
func main() {
// new(T) allocates a zeroed T and returns a *T pointer to it.
p := new(int) // p is *int, *p is 0
*p = 7
fmt.Println(*p) // 7
// The zero value of any pointer is nil. Dereferencing nil PANICS,
// so guard with a check when a pointer might be unset.
var q *int
fmt.Println(q == nil) // true
// With structs, Go auto-dereferences field access through a pointer:
// you write cp.Hits, not (*cp).Hits.
c := Counter{}
cp := &c
cp.Hits++ // same as (*cp).Hits++
cp.Hits++
fmt.Println(c.Hits) // 2 (original struct changed)
}Your turn — take an address, then write through the pointer.
package main
import "fmt"
func main() {
// 🎯 YOUR TURN — replace each ___ then run it.
score := 5
// 1) Take the address of score
p := ___score // 👉 &score
// 2) Change score to 9 by writing THROUGH the pointer
___ = 9 // 👉 *p
fmt.Println("score is", score)
// ✅ Expected output: score is 9
}Now pass an address so a function can reset your variable.
package main
import "fmt"
func reset(n *int) {
___ = 0 // 👉 *n (set the pointed-at value to 0)
}
func main() {
// 🎯 YOUR TURN — pass the address so reset can change lives.
lives := 3
reset(___lives) // 👉 &lives
fmt.Println("lives:", lives)
// ✅ Expected output: lives: 0
}🧩 Reorder Challenge
These lines change a variable through a pointer, but they're scrambled. Put them in order so the program prints 42 (assume a func main wraps them).
*p = 42
p := &x
x := 1
fmt.Println(x)- fmt.Println(x)
Why: x must exist before you can take its address. p := &x captures that address; *p = 42 writes through the pointer into x. Printing x last shows 42 because the write changed the original variable.
Pro Tips
- 💡 Reach for a pointer to mutate or to avoid copying a big struct — otherwise a plain value is simpler and safer.
- 💡 No pointer arithmetic in Go. You can't do p + 1 like in C — Go pointers only point, which removes a whole class of bugs.
- 💡 Let the GC handle memory. Returning &localVar from a function is safe — Go keeps it alive automatically.
- 💡 Always nil-check a pointer that might be unset before dereferencing it, or you'll hit a runtime panic.
Common Errors (and the fix)
- "invalid memory address or nil pointer dereference" — you dereferenced a nil pointer. Initialise it (with new, &something, or a literal) and nil-check before using.
- "cannot take the address of ..." — you tried & on something with no address, like a map element or a literal. Store it in a variable first.
- Function change doesn't stick — you passed a value, not a pointer. Use a *T parameter and call with &x to mutate the caller's data.
- "mismatched types int and *int" — you forgot to dereference. Compare *p == 5, not p == 5.
📋 Quick Reference
| Operation | Syntax | Meaning |
|---|---|---|
| Address-of | p := &x | p is *int pointing at x |
| Dereference (read) | v := *p | value at p |
| Dereference (write) | *p = 9 | changes the original |
| Allocate | p := new(int) | zeroed *int |
| Zero value | var p *int | nil |
| Struct field | p.Field | auto-dereferenced |
🧠 Quick Recall
Predict the output before you reveal the answer.
x := 5; p := &x; *p = 8; fmt.Println(x)
8 — p points at x, so writing *p = 8 changes the original x to 8.
func f(n int) { n = 0 }; x := 4; f(x); fmt.Println(x)
4 — f takes n by value, so it edits a copy. The original x is unchanged. (Pass *int to mutate it.)
var p *int; fmt.Println(p == nil)
true — the zero value of any pointer type is nil. Dereferencing it would panic, so always check first.
Frequently Asked Questions
Q: When should I use a pointer?
Two main reasons: to let a function mutate the caller's value, and to avoid copying a large struct on every call. For small, read-only values, a plain value is clearer.
Q: Is Go pointer code dangerous like C?
Much safer. There's no pointer arithmetic, memory is garbage-collected, and returning the address of a local variable is perfectly fine — Go keeps it alive for you.
Q: Why can I write p.Field instead of (*p).Field?
Go automatically dereferences a struct pointer when you access a field or call a method, purely as a convenience. Both forms mean the same thing.
Mini-Challenge: Deposit Through a Pointer
Write a deposit(a *Account, amount int) that adds to a.Balance, then call it with &acc. Because you pass the address, the change is visible in main. Write it yourself and match the example output.
package main
import "fmt"
type Account struct {
Balance int
}
// 🎯 MINI-CHALLENGE: Deposit through a pointer
// Write a function deposit(a *Account, amount int) that adds
// amount to a.Balance. Because you pass a pointer, the change
// should be visible back in main.
//
// ✅ Example output: balance: 250
func main() {
acc := Account{Balance: 200}
// call deposit here to add 50
fmt.Println("balance:", acc.Balance)
}🎉 Lesson Complete!
- ✅ &x takes an address; *p dereferences to read or write the value
- ✅ Writing through a pointer changes the original variable
- ✅ Pass a *T parameter (and call with &x) so a function can mutate the caller's data
- ✅ new(T) allocates a zeroed value; every pointer's zero value is nil
- ✅ Go auto-dereferences struct fields: p.Field works directly — and there's no pointer arithmetic
Practice quiz
What does the & operator do in Go?
- Dereferences a pointer
- Takes the address of a variable
- Multiplies values
- Declares a pointer type
Answer: Takes the address of a variable. &x yields a pointer holding the memory address of x.
What does the * operator do when applied to a pointer value?
- Takes its address
- Dereferences it to read or write the pointed-to value
- Frees memory
- Compares pointers
Answer: Dereferences it to read or write the pointed-to value. *p dereferences the pointer to access the value it points to.
What is the zero value of a pointer?
- 0
- nil
- An empty struct
- Undefined
Answer: nil. An uninitialized pointer is nil.
What happens if you dereference a nil pointer?
- Returns 0
- Returns nil
- Runtime panic
- Compile error
Answer: Runtime panic. Dereferencing a nil pointer causes a runtime panic (invalid memory address).
After p := &x; *p = 20, what is the value of x?
- 1
- 20
- 0
- nil
Answer: 20. Writing through the pointer with *p = 20 changes the original variable x to 20.
How can a function modify the caller's variable?
- Pass it by value
- Take a *T parameter and call with &x
- Return it only
- Use a global
Answer: Take a *T parameter and call with &x. Pass a pointer (*T) and call with the address (&x) so the function mutates the original.
What does new(T) return?
- A value of type T
- A pointer to a zeroed T
- nil
- An error
Answer: A pointer to a zeroed T. new(T) allocates a zeroed T and returns a *T pointing to it.
Does Go support pointer arithmetic like C (p++)?
- Yes
- No
- Only for slices
- Only with unsafe
Answer: No. Ordinary Go has no pointer arithmetic — pointers can't be incremented.
Given p *Account, how do you read a struct field through it?
- (*p).Field only
- p.Field — Go auto-dereferences
- p->Field
- Field(p)
Answer: p.Field — Go auto-dereferences. Go auto-dereferences struct pointers, so p.Field works directly.
Passing a large struct by pointer instead of by value mainly avoids:
- Type errors
- Copying the whole struct
- Imports
- Panics
Answer: Copying the whole struct. A pointer passes an address, avoiding a full copy of the struct's data.
Continue this course
- Previous: Maps
- Next: Structs and Interfaces — Group related data with structs and write flexible code with interfaces