willworth.dev
View RSS feed

Complete Guide to Pointers in Go: From Basics to Best Practices

Published on

Complete Guide to Pointers in Go

Quick Reference

// Declaration
var p *int // Declare pointer to int (nil by default)
x := 42 // Regular variable
p = &x // & operator: get address of x
value := *p // * operator: get value at address (dereferencing)

// Common Operations
*p = 100 // Modify value through pointer
fmt.Println(&x) // Print memory address
fmt.Println(*p) // Print value at address

// Create & Initialize
p = new(int) // Allocate memory for int, return pointer
p = &int(100) // Create int and get its pointer

// Struct Pointers
type Person struct {
Name string
}
person := &Person{"Alice"} // Create struct and get pointer
fmt.Println(person.Name) // Auto-dereferencing for structs

Understanding Pointers from First Principles

What is a Pointer?

Think of memory like a giant apartment building:

  • Each apartment (memory location) has an address
  • Each apartment can store one value
  • A pointer is like having someone's address written down
  • When you have their address, you can:
    • Go there to see what's inside (dereferencing)
    • Go there to change what's inside (modifying through pointer)
x := 42 // Create an "apartment" with 42 in it
p := &x // Write down this apartment's address
fmt.Println(*p) // Go to address, report what's inside (42)
*p = 100 // Go to address, put 100 inside instead

The Core Operations

  1. Getting an Address (&)

    x := 42
    p := &x // p now holds x's memory address

    Think: "Give me the address of x"

  2. Following an Address (*)

    value := *p // Get the value at p's address
    *p = 100 // Change the value at p's address

    Think: "Go to this address and..."

  3. Declaring Pointer Types (*Type)

    var p *int // p can hold the address of an int
    var s *string // s can hold the address of a string

    Think: "This variable holds the address of a..."

Why Use Pointers?

1. Modifying Function Parameters

In Go, everything is pass-by-value. Pointers let you modify the original:

// Without pointer (changes are lost)
func birthday(age int) {
age++
}

// With pointer (changes persist)
func birthdayPtr(age *int) {
*age++
}

myAge := 30
birthday(myAge) // myAge is still 30
birthdayPtr(&myAge) // myAge is now 31

2. Efficient Memory Usage

Passing large structs by pointer avoids copying:

type HugeStruct struct {
Data [1000000]int
}

// Efficient: just passes a memory address
func processHuge(h *HugeStruct) {
// Work with h
}

huge := &HugeStruct{}
processHuge(huge)

3. Optional Values with nil

Pointers can be nil, making them perfect for optional values:

type Config struct {
Port int
Timeout *int // Optional timeout
}

// No timeout specified
config1 := Config{Port: 8080}

// With timeout
timeout := 30
config2 := Config{
Port: 8080,
Timeout: &timeout,
}

// Safe usage
if config.Timeout != nil {
fmt.Printf("Timeout set to: %d\n", *config.Timeout)
}

Common Patterns and Techniques

1. Constructor Functions

Return pointers to prevent unnecessary copying:

type Person struct {
Name string
Age int
}

func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}

person := NewPerson("Alice", 30)

2. Method Receivers

Choose between pointer and value receivers:

type Counter struct {
value int
}

// Value receiver (changes don't persist)
func (c Counter) Display() {
fmt.Println(c.value)
}

// Pointer receiver (changes persist)
func (c *Counter) Increment() {
c.value++
}

3. Working with Slices and Maps

Remember: slices and maps are already reference types:

// No pointer needed for basic operations
func addToSlice(s []int, val int) {
s = append(s, val) // WARNING: This might not work as expected!
}

// Use pointer if you need to modify the slice header
func addToSlicePtr(s *[]int, val int) {
*s = append(*s, val) // This works correctly
}

Advanced Topics and Gotchas

1. Pointer Arithmetic

Go doesn't allow pointer arithmetic (unlike C):

p := &x
p++ // Compilation error!

2. Pointer to Pointer

Sometimes you need a pointer to a pointer:

var x int = 42
var p *int = &x
var pp **int = &p

fmt.Println(**pp) // Prints 42

3. Interface Implementation

Methods with pointer receivers only satisfy interfaces when using pointers:

type Incrementer interface {
Increment()
}

type Number struct {
value int
}

// Pointer receiver
func (n *Number) Increment() {
n.value++
}

var i Incrementer
n := Number{value: 42}
i = &n // Works
i = n // Compilation error!

Best Practices

Do:

✅ Use pointers for methods that modify receivers ✅ Use pointers for large structs to avoid copying ✅ Use pointers for optional values (can be nil) ✅ Check for nil before dereferencing ✅ Use pointer receivers consistently in types

Don't:

❌ Use pointers for small structs or basic types unnecessarily ❌ Return pointers to loop variables ❌ Forget to check for nil when it's a possibility ❌ Use pointers just because you can

Safety Features in Go

Unlike C, Go provides several safety features:

  1. No pointer arithmetic
  2. No direct memory access
  3. Garbage collection
  4. Type safety
  5. Nil pointer checks

Common Mistakes and Solutions

1. The Loop Variable Trap

// WRONG
var ptrs []*int
for i := 0; i < 3; i++ {
ptrs = append(ptrs, &i) // All pointers will point to the same address
}

// RIGHT
for i := 0; i < 3; i++ {
val := i
ptrs = append(ptrs, &val)
}

2. Returning Local Variables

// WRONG
func createPointer() *int {
x := 42
return &x // Actually safe in Go! The variable escapes to heap
}

// ALSO FINE
func createPointer() *int {
return new(int)
}

3. Nil Pointer Dereference

// WRONG
func process(p *int) {
fmt.Println(*p) // Might panic!
}

// RIGHT
func process(p *int) {
if p == nil {
return
}
fmt.Println(*p)
}

Conclusion

Pointers in Go provide a powerful way to manage memory and share data efficiently. While they might seem intimidating at first, Go's safety features make them much safer to use than in languages like C. Remember:

  • Use pointers when you need to modify values through functions
  • Use pointers for large structs to avoid copying
  • Use pointers for optional values that can be nil
  • Always check for nil when there's a possibility
  • Don't overuse pointers - Go's built-in types often provide what you need

The key is finding the right balance - use pointers when they provide clear benefits, but don't make your code needlessly complex by using them everywhere.