Pointers in Go Explained for People Who Fear Pointers
A pointer is a variable that holds another variable's address. Learn & and *, nil, new, pointers to structs and slice elements, and when a pointer helps.

The usual fear of pointers comes from languages where a pointer can point anywhere, be added to, be freed twice and be left dangling. Go keeps the useful half and removes the dangerous half. There is no pointer arithmetic, memory is garbage collected so there is nothing to free, and the compiler will not let a pointer outlive what it points at. What remains is a small idea: a pointer is a value that says where another value lives. Most of the confusion afterwards is about which copy you are holding. This guide builds the model from & and * upward, runs each rule, and finishes with how to decide between a pointer and a value. It follows structs and arrays in the Go basics path, and it is the background for pointer or value receivers.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
Two operators and one idea
The type *int means "a pointer to an int". The expression &x gives the address of the variable x, and the expression *p follows a pointer to the value behind it. Reading and writing through *p read and write the original variable:
package main
import "fmt"
func main() {
x := 10
p := &x
fmt.Println(x, *p, p == &x)
*p = 20
fmt.Println(x, *p)
x = 30
fmt.Println(*p)
var np *int
fmt.Println(np == nil, np)
}10 10 true
20 20
30
true <nil>After p := &x, x and *p are two names for one variable. Writing through the pointer changed x, and changing x is visible through the pointer. Two pointers are equal when they hold the same address, which is why p == &x is true. The last line shows the zero value: an uninitialized pointer is nil, prints as <nil>, and points at nothing. Dereferencing it is a runtime panic. I deliberately avoid printing addresses in these examples, since they change from run to run and mean nothing on their own. If you want to see one, %p prints it.
Go passes everything by value
This single rule explains most pointer questions. Calling a function copies each argument into the callee's parameter. For an int that means the callee gets its own integer. To let a function change the caller's variable, give it a pointer:
package main
import "fmt"
func doubleValue(n int) {
n *= 2
}
func doublePointer(n *int) {
*n *= 2
}
func reassign(n *int) {
other := 99
n = &other
}
func main() {
a := 5
doubleValue(a)
fmt.Println(a)
doublePointer(&a)
fmt.Println(a)
reassign(&a)
fmt.Println(a)
}5
10
10doubleValue changed its own copy, so a stayed at 5. doublePointer received a copy of the address and followed it, so a became 10. The third function is the one that explains the model. reassign changes where its own copy of the pointer points, to a new variable, and the caller's a is unaffected. Passing a pointer gives the callee a way to edit the pointed-to value. It does not give it a way to repoint your variable. If a function needs to do that, take a pointer to the pointer, which is rare and almost always a sign to return a value instead.
Slices, maps and channels already contain pointers internally, so passing them shares the underlying data, as explained in slices from the header to the backing array. You do not need *[]int to let a function change elements. You do need it, or a return value, if the function must change the slice's length for the caller.
Pointers to structs
Structs are where you use pointers most. The compiler lets you use the same dot for a struct and a pointer to a struct, so you rarely write (*p).field:
package main
import "fmt"
type Node struct {
Val int
Next *Node
}
func (n *Node) Len() int {
if n == nil {
return 0
}
return 1 + n.Next.Len()
}
func main() {
list := &Node{1, &Node{2, &Node{3, nil}}}
fmt.Println(list.Val, list.Next.Val, list.Len())
var empty *Node
fmt.Println(empty.Len())
copyOfFirst := *list
copyOfFirst.Val = 100
fmt.Println(list.Val, copyOfFirst.Val, copyOfFirst.Next == list.Next)
}1 2 3
0
1 100 true&Node{...} allocates a node and returns its address, which is the idiomatic way to create a struct you intend to share. The chain of three nodes is a linked list. The Len method on *Node handles a nil receiver, which is legal: a method call on a nil pointer is fine until the method touches a field, and handling nil explicitly turns the end of a list into the base case of the recursion. That is why empty.Len() prints 0 instead of panicking. The last block shows that *list makes a copy of the struct, with the same Next pointer, so the copy shares the rest of the chain. Copying a struct is shallow, as covered in the structs guide.
What a nil dereference looks like
If the method had read n.Val before checking for nil, you would meet the most common runtime error in Go:
package main
import "fmt"
type Node struct {
Val int
Next *Node
}
func main() {
var n *Node
fmt.Println(n.Val)
}panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x100dc1470]
goroutine 1 [running]:
main.main()
example.test/lab/main.go:12 +0x20
...The message is panic: runtime error: invalid memory address or nil pointer dereference, followed by [signal SIGSEGV: segmentation violation ...]. The stack trace names the line, and the usual causes and fixes are in the nil pointer dereference article. The defense in code is to check for nil before a dereference when nil is a legitimate state, and to make constructors return fully initialized values so that it is not.
new, composite literals and returning pointers to locals
There are two ways to allocate a zeroed value and get a pointer: new(T) and &T{...}. For structs they are interchangeable, and &T{} is preferred because it lets you set fields. new is mostly used for basic types, p := new(int), which is why you will see it rarely. The surprising part for C programmers is that returning the address of a local variable is not just allowed, it is normal:
package main
import (
"fmt"
"testing"
)
type Point struct{ X, Y int }
func byValue() Point { return Point{1, 2} }
func byPointer() *Point { return &Point{1, 2} }
var sinkValue Point
var sinkPointer *Point
func newCounter() *int {
c := 0
return &c
}
func main() {
next := newCounter()
*next++
*next++
fmt.Println(*next)
fmt.Println("value:", testing.AllocsPerRun(100, func() { sinkValue = byValue() }))
fmt.Println("pointer:", testing.AllocsPerRun(100, func() { sinkPointer = byPointer() }))
}2
value: 0
pointer: 1newCounter returns the address of c, and the counter keeps working after the function has returned. The compiler's escape analysis notices that c outlives the call and puts it on the heap instead of the stack. You never decide this. The allocation counts show the cost: returning a Point by value allocated nothing, and returning a pointer to one allocated once, because the value had to live on the heap. For a three-word struct that is a single small allocation and it is cheap. In a loop over millions of items it is millions of allocations for the garbage collector to find. The details are in stack versus heap in Go, and the practical rule is below.
Pointers into slices, and maps that refuse
A pointer to a slice element points into the backing array. It stays valid for exactly as long as that array is the slice's array, which means until an append reallocates it:
package main
import "fmt"
func main() {
s := make([]int, 1, 1)
p := &s[0]
s = append(s, 2)
*p = 99
fmt.Println(s[0], *p)
t := make([]int, 1, 10)
q := &t[0]
t = append(t, 2)
*q = 99
fmt.Println(t[0], *q)
}0 99
99 99In the first case the append had no room, so it allocated a new array and copied. The pointer p still points into the old array, and the write through p changed memory that the slice no longer uses. The slice prints 0 while the pointer reads 99. In the second case there was spare capacity, so both views share the array and agree. The program is correct in both cases and the behavior depends on capacity, which is why holding pointers into a slice that you also append to is a recipe for the aliasing trap described in the append gotcha. Take the index and re-read through the slice, or store pointers as the elements.
Maps go further. The address of a map element cannot be taken at all, because the table may move entries when it grows. &m["k"] is a compile error, which is the same reason m["k"].Field = 1 fails for struct values. Store pointers as the map's values if you need to modify entries in place, as maps in Go shows.
Pointers and loop variables
Before Go 1.22, a for loop reused one variable for every iteration, so taking its address in the loop gave you the same pointer each time. The result was a slice of pointers that all showed the last value. Since 1.22, each iteration has its own copy of the variable when the module declares Go 1.22 or later:
package main
import "fmt"
func main() {
var ptrs []*int
for i := 0; i < 3; i++ {
ptrs = append(ptrs, &i)
}
for _, p := range ptrs {
fmt.Print(*p, " ")
}
fmt.Println()
}0 1 2With this module's go 1.25 setting, the output is 0 1 2. On a module that says go 1.21 the same source prints 3 3 3. If you work on older code, the loop variable change explains how to check, and the go line in go.mod is what controls the behavior, not the installed toolchain.
When a pointer is the right choice
I reach for a pointer in five situations, and the default for everything else is a plain value.
- The function or method must change the value. A pointer receiver or pointer parameter is the only way.
- The value is large. Passing a kilobyte struct by value copies it on every call. The benchmark showed 31 ns against 2 ns for a 1.1 KB struct.
- The value must not be copied. Anything with a
sync.Mutex, abytes.Bufferor an open resource. - "Absent" is a real state. An optional field in a JSON patch is
*string, sonilmeans not provided. Structs shows a patch type. - Several holders must see the same value. A shared cache entry, a node in a graph.
For small values, such as an int, a bool or a three-word struct, a pointer costs more than it saves. It adds a nil check, a possible heap allocation and a level of indirection that the CPU has to chase. Do not take *int parameters to "avoid copying" an integer, as the copy is the same size as the pointer.
Identity or equality: comparing pointers
Two pointers are equal when they point at the same variable, not when the values behind them happen to match. That distinction matters when pointers are map keys or are compared in conditions:
package main
import "fmt"
type User struct{ Name string }
func main() {
a := &User{"ada"}
b := &User{"ada"}
c := a
fmt.Println(a == b, a == c, *a == *b)
seen := map[*User]bool{a: true}
fmt.Println(seen[a], seen[b], seen[c])
}false true true
true false truea and b hold equal values at different addresses, so the pointers differ while the dereferenced structs are equal. c is a copy of the pointer a, so they are the same. A map keyed by pointer therefore tracks object identity: b is a stranger even though it looks the same. That is exactly right for a visited set in a graph walk, where the question is "have I been to this node", and exactly wrong for a cache keyed by user data, where the question is "have I seen this value". Key by the field that identifies the thing, an ID or a name, and keep pointers for the cases where identity is the point.
Changing a slice through a function
The pass-by-value rule shows up again with slices. A function that appends to its parameter changes only its own header, so the caller does not see the new element:
package main
import "fmt"
func addValue(s []int, n int) {
s = append(s, n)
}
func addPointer(s *[]int, n int) {
*s = append(*s, n)
}
func addReturn(s []int, n int) []int {
return append(s, n)
}
func main() {
var s []int
addValue(s, 1)
fmt.Println(len(s))
addPointer(&s, 2)
fmt.Println(s)
s = addReturn(s, 3)
fmt.Println(s)
}0
[2]
[2 3]The first call is lost, since addValue appended to a copy of the header. The second works because the function wrote through a pointer to the caller's slice variable. The third is how the standard library does it: append itself returns the new slice, and the caller assigns it. I prefer the returning form. It makes the data flow visible at the call site, avoids a pointer to a slice that exists only to be mutated, and cannot be misused by passing a nil pointer. The pointer form is reasonable when a method on a struct owns the slice, as in func (s *Stack) Push(v int) { s.items = append(s.items, v) }, because there the pointer receiver is what carries the change.
A small stack, with the pointers visible
A stack built as a linked list shows nil handling and pointer receivers in one place. Each push allocates a node that points at the previous top. Each pop moves the top down:
package main
import (
"errors"
"fmt"
)
type node struct {
val int
next *node
}
type Stack struct {
top *node
size int
}
func (s *Stack) Push(v int) {
s.top = &node{val: v, next: s.top}
s.size++
}
func (s *Stack) Pop() (int, error) {
if s.top == nil {
return 0, errors.New("pop from empty stack")
}
v := s.top.val
s.top = s.top.next
s.size--
return v, nil
}
func main() {
var s Stack
for i := 1; i <= 3; i++ {
s.Push(i * 10)
}
for range 4 {
v, err := s.Pop()
fmt.Println(v, err, s.size)
}
}30 <nil> 2
20 <nil> 1
10 <nil> 0
0 pop from empty stack 0The zero value of Stack is an empty stack, with a nil top and a size of zero, so var s Stack is ready to use with no constructor. Pop checks for nil before it touches the node, which is the discipline that prevents the nil dereference panic. And each popped node becomes unreachable once top moves past it, so the garbage collector reclaims it without any code from you. In practice a slice-backed stack is faster and simpler, because the elements sit next to each other in memory and append does the allocation work. A linked list earns its place when you need cheap insertion in the middle, and that is a less common need than textbooks suggest.
Three questions for pointer-heavy code
When you read code that is full of pointers, ask these in order. Who owns the value, meaning who is allowed to change it? If two parts of the program hold the same pointer, is that intended, or is it an accident of a shallow copy? And can this pointer be nil here, and what happens if it is? Code that has good answers to all three tends to be fine, and code where the answers are unclear is where the nil panics and the surprise mutations come from. Writing the answers as a comment on the field, "set by the constructor, never nil", costs a line and saves the next reader a debugging session.
Pointers and interfaces
Interfaces add one subtlety. An interface value holds a type and a value, and a nil pointer stored in an interface is not a nil interface. That is the nil interface trap, and it bites when a function returns a typed nil pointer as an error. The other point is method sets: a value of type T has only value-receiver methods, and *T has both, so putting a T in an interface that needs a pointer-receiver method is a compile error. The check is described in pointer or value receiver.
Pointers shared across goroutines
A pointer is also how two goroutines come to see the same variable, and Go does not add any protection on top of that. If one goroutine writes through a pointer while another reads it, that is a data race, and the result can be a torn value or a stale one. The fix is the usual set: a mutex around the access, an atomic type for a single counter or flag, or a design where only one goroutine owns the pointer and others send it requests over a channel. Handing a pointer to a goroutine is a transfer of responsibility, and a comment saying who may touch it afterwards is worth writing. The race detector, enabled with go test -race, will find the cases you missed.
What Go deliberately leaves out
You cannot add 1 to a pointer, cast an integer to a pointer or compare a pointer's address as a number in safe Go. The unsafe package breaks all three and exists for the runtime, for cgo and for a few low-level libraries. If a design seems to need unsafe.Pointer, there is almost always a slice, an index or an interface that does the job. There is also nothing to free. The garbage collector keeps anything that is still reachable through a pointer alive, which is why holding a pointer to a tiny field inside a huge struct keeps the whole struct in memory, a leak pattern that the slice article calls out.
Where this advice stops
If your program is mostly small values and plain structs, you may use pointers rarely, and that is a good sign. Systems code that manages large graphs, or code that wraps C libraries, uses them heavily and has to think about ownership by convention, since the compiler does not track it. If you are reading Go that seems to use pointers everywhere, check whether the author came from a language where objects are references, and whether plain values would be simpler. The official references are the specification on pointer types and the Go FAQ entry on pointers and values, which says directly that Go is always call-by-value.


