Pointers
Pointers in Go are simpler than C and more explicit than Java. There is no pointer arithmetic, no manual memory management, and no void pointers. Go pointers exist for one purpose: to let you share a value across function boundaries so the callee can read or modify the original.
The Basics: & and *
Two operators are all you need.
&takes the address of a variable (gives you a pointer)*dereferences a pointer (gives you the value it points to)
x := 42
p := &x // p is *int, pointing to x
fmt.Println(*p) // 42 -- dereference to get the value
*p = 100 // Modify x through the pointer
fmt.Println(x) // 100
The type *int means "pointer to int." The & operator creates a pointer. The * operator follows the pointer to the value.
func double(n *int) {
*n *= 2
}
func main() {
val := 21
double(&val)
fmt.Println(val) // 42
}
Nil Pointers
A pointer with no assigned value is nil. Dereferencing a nil pointer causes a panic at runtime.
var p *int // nil
fmt.Println(p) // <nil>
// fmt.Println(*p) // PANIC: runtime error: invalid memory address
// Always check before dereferencing
if p != nil {
fmt.Println(*p)
}
Nil pointers are useful for representing optional values. If a function returns *User, returning nil clearly means "no user found."
func findUser(id int) *User {
user, ok := users[id]
if !ok {
return nil
}
return &user
}
func main() {
u := findUser(42)
if u == nil {
fmt.Println("user not found")
return
}
fmt.Println(u.Name)
}
When to Use Pointers
Mutation
The most common reason. If a function needs to modify its argument, it needs a pointer.
type Config struct {
Host string
Port int
Verbose bool
}
func applyDefaults(cfg *Config) {
if cfg.Host == "" {
cfg.Host = "localhost"
}
if cfg.Port == 0 {
cfg.Port = 8080
}
}
func main() {
cfg := Config{Verbose: true}
applyDefaults(&cfg)
fmt.Printf("%s:%d verbose=%v\n", cfg.Host, cfg.Port, cfg.Verbose)
}
localhost:8080 verbose=true
Without the pointer, applyDefaults would modify a copy and the caller's cfg would remain unchanged.
Optional Values
Pointers can represent "present or absent" since they can be nil.
type UpdateRequest struct {
Name *string // nil means "don't update"
Email *string // nil means "don't update"
Age *int // nil means "don't update"
}
func updateUser(id int, req UpdateRequest) error {
user, err := getUser(id)
if err != nil {
return err
}
if req.Name != nil {
user.Name = *req.Name
}
if req.Email != nil {
user.Email = *req.Email
}
if req.Age != nil {
user.Age = *req.Age
}
return saveUser(user)
}
This pattern is common for PATCH-style API endpoints where you need to distinguish between "set this field to empty" and "leave this field unchanged."
Large Structs
Passing a large struct by value copies all of it. A pointer avoids the copy.
type Report struct {
Title string
Sections []Section
Data [10000]float64 // Large fixed-size array
}
// Bad: copies the entire Report (including the 80KB array) every call
func summarize(r Report) string {
return fmt.Sprintf("%s: %d sections", r.Title, len(r.Sections))
}
// Good: passes an 8-byte pointer regardless of struct size
func summarize(r *Report) string {
return fmt.Sprintf("%s: %d sections", r.Title, len(r.Sections))
}
When Not to Use Pointers
Small Structs, Read-Only
If a struct is small and you only need to read it, pass by value. The copy is cheap and you avoid nil-checking and aliasing concerns.
type Point struct {
X, Y float64
}
// Value semantics: simple, safe, no nil to worry about
func distance(a, b Point) float64 {
dx := a.X - b.X
dy := a.Y - b.Y
return math.Sqrt(dx*dx + dy*dy)
}
Slices, Maps & Channels
These types already contain internal pointers. Passing them by value does not copy the underlying data.
// No pointer needed -- the slice header is small (24 bytes)
// and the underlying array is shared
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
A []int is a struct of three fields: a pointer, a length, and a capacity. Passing it by value copies the header but shares the underlying array. Same for maps and channels.
Strings
Strings in Go are immutable and internally represented as a pointer-length pair (16 bytes). Passing string by value is always cheap.
// No pointer needed for strings
func greet(name string) string {
return "Hello, " + name
}
Pointer Receivers on Methods
Methods with pointer receivers can modify the struct and are the standard choice when the struct represents mutable state.
type Stack struct {
items []int
}
func (s *Stack) Push(val int) {
s.items = append(s.items, val)
}
func (s *Stack) Pop() (int, bool) {
if len(s.items) == 0 {
return 0, false
}
val := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return val, true
}
func (s *Stack) Len() int {
return len(s.items)
}
func main() {
var s Stack
s.Push(1)
s.Push(2)
s.Push(3)
for s.Len() > 0 {
val, _ := s.Pop()
fmt.Println(val)
}
}
3
2
1
Go automatically takes the address when calling a pointer receiver method on an addressable value. That is why s.Push(1) works even though s is not a pointer -- the compiler rewrites it to (&s).Push(1).
No Pointer Arithmetic
Go deliberately omits pointer arithmetic. You cannot increment a pointer to walk through memory, and you cannot cast between arbitrary pointer types.
// This is not Go. None of this compiles.
// p++
// p += sizeof(int)
// q := (*float64)(p)
The unsafe package exists for low-level memory manipulation, but it is explicitly named to signal that you are leaving Go's safety guarantees. Standard application code should never need it.
Common Pitfalls
- Nil pointer panics. The most common runtime crash in Go. When a function returns a pointer, check for nil before using it. This is especially important with map lookups and type assertions that return pointers.
- Unnecessary pointers. Using
*stringor*intwhen the zero value would work fine. Only use pointer fields for genuinely optional values. - Pointer to loop variable. Before Go 1.22, the loop variable was reused across iterations. Taking its address inside a loop gave you a pointer to the last iteration's value.
// Bug in Go < 1.22
var ptrs []*int
for _, v := range []int{1, 2, 3} {
ptrs = append(ptrs, &v) // All point to the same variable
}
// ptrs[0], ptrs[1], ptrs[2] all point to 3
// Fixed in Go 1.22+ (loop variable is per-iteration)
// For older Go versions, capture the variable:
for _, v := range []int{1, 2, 3} {
v := v // Shadow the loop variable
ptrs = append(ptrs, &v)
}
- Confusing pointer equality with value equality. Two pointers are equal (
==) only if they point to the same memory, not if the values they point to are the same. - Returning pointers to interface values.
*io.Readeris rarely what you want. Interfaces already hold a pointer internally. - Premature optimization with pointers. Passing a 3-field struct by value costs nanoseconds. Do not add pointer indirection to save a trivial copy. Profile first.
Key Takeaways
&gives you a pointer.*follows a pointer. That is all the syntax there is.- Nil pointers are useful for optional values but will panic if dereferenced unchecked.
- Use pointers for mutation, optional values, and large structs. Use values for small, read-only data.
- Slices, maps, channels, and strings already contain internal pointers. Do not add another layer.
- Go has no pointer arithmetic. The
unsafepackage exists but should not appear in application code. - Escape analysis moves heap-escaping values automatically. Trust the compiler and GC until profiling says otherwise.
- Before Go 1.22, taking the address of a loop variable was a classic bug. In Go 1.22+, loop variables are per-iteration.