🎯 Pointers in Go

Master memory addresses, pointer operations, and efficient data manipulation through indirect references

📚 Theory: Understanding Pointers

Pointers are variables that store memory addresses rather than values directly. They provide indirect access to data and enable efficient memory management, data sharing, and modification of function arguments.

What Are Pointers?

A pointer is a variable that holds the memory address of another variable. In Go, pointers are typed - a pointer to an int (*int) can only point to int variables. This type safety prevents many common pointer-related errors found in other languages.

Memory Model

Stack x (int) Address: 0xc000014080 42 p (*int) Address: 0xc000014088 0xc000014080 Heap new(int) Address: 0xc000100000 0

Pointer Operations

Operation Syntax Description Example
Address-of & Get memory address p := &x
Dereference * Access value at address val := *p
Declaration *Type Declare pointer type var p *int
Allocation new() Allocate zero-valued memory p := new(int)
Nil check == nil Check if pointer is nil if p == nil

Basic Pointer Usage

package main

import "fmt"

func main() {
    // Declare a variable
    x := 42
    
    // Create a pointer to x
    p := &x  // & gets the address
    
    fmt.Printf("Value of x: %d\n", x)
    fmt.Printf("Address of x: %p\n", &x)
    fmt.Printf("Value of p: %p\n", p)
    
    // Dereference pointer to get value
    fmt.Printf("Value at address p: %d\n", *p)
    
    // Modify value through pointer
    *p = 100
    fmt.Printf("New value of x: %d\n", x)
    
    // Pointer types
    var intPtr *int      // nil by default
    var floatPtr *float64 // nil by default
    
    // Check for nil
    if intPtr == nil {
        fmt.Println("intPtr is nil")
    }
    
    // Allocate memory for pointer
    intPtr = new(int)  // *intPtr is 0
    *intPtr = 55
    fmt.Printf("Value: %d\n", *intPtr)
}

🔄 Pointers and Functions

Pointers are essential for functions that need to modify their arguments or avoid copying large data structures. Understanding pass-by-value vs pass-by-reference is crucial for efficient Go programming.

Pass by Value

Go passes all arguments by value (creates copies). Changes to parameters don't affect original variables.

  • Safe from unintended modifications
  • Can be inefficient for large structs
  • Original data remains unchanged

Pass by Reference (Pointer)

Passing pointers allows functions to modify original data and avoids copying overhead.

  • Efficient for large data structures
  • Enables in-place modifications
  • Requires careful nil checking
// Pass by value - doesn't modify original
func doubleValue(x int) {
    x = x * 2
}

// Pass by pointer - modifies original
func doublePointer(x *int) {
    *x = *x * 2
}

// Return pointer
func createUser(name string) *User {
    return &User{
        Name:      name,
        CreatedAt: time.Now(),
    }
}

// Struct with pointer fields
type Node struct {
    Value int
    Next  *Node  // Pointer to next node
}

// Modify struct through pointer
func (n *Node) Append(value int) {
    for n.Next != nil {
        n = n.Next
    }
    n.Next = &Node{Value: value}
}

func main() {
    num := 10
    
    doubleValue(num)
    fmt.Printf("After doubleValue: %d\n", num)  // Still 10
    
    doublePointer(&num)
    fmt.Printf("After doublePointer: %d\n", num)  // Now 20
    
    // Linked list example
    head := &Node{Value: 1}
    head.Append(2)
    head.Append(3)
    
    // Traverse list
    current := head
    for current != nil {
        fmt.Printf("%d -> ", current.Value)
        current = current.Next
    }
}

🔢 Pointers and Collections

Go's approach to pointers with arrays and slices differs significantly from languages like C. Go prioritizes safety by disallowing pointer arithmetic while still providing efficient access patterns.

⚠️ No Pointer Arithmetic

Go intentionally omits pointer arithmetic to prevent buffer overflows and memory corruption. Use slices for safe, efficient array operations instead.

// Arrays and pointers
func modifyArray(arr *[3]int) {
    arr[0] = 100  // Same as (*arr)[0] = 100
    arr[1] = 200
    arr[2] = 300
}

// Slices are reference types (contain pointers)
func modifySlice(s []int) {
    for i := range s {
        s[i] *= 2
    }
}

// Pointer to slice (rarely needed)
func appendToSlice(s *[]int, values ...int) {
    *s = append(*s, values...)
}

func main() {
    // Array pointer
    arr := [3]int{1, 2, 3}
    modifyArray(&arr)
    fmt.Println(arr)  // [100 200 300]
    
    // Slice (already reference type)
    slice := []int{1, 2, 3}
    modifySlice(slice)
    fmt.Println(slice)  // [2 4 6]
    
    // Pointer to slice
    nums := []int{1, 2, 3}
    appendToSlice(&nums, 4, 5, 6)
    fmt.Println(nums)  // [1 2 3 4 5 6]
    
    // Working with unsafe (rarely needed)
    import "unsafe"
    
    x := [3]int{1, 2, 3}
    ptr := &x[0]
    size := unsafe.Sizeof(int(0))
    
    // Access elements (not recommended)
    for i := 0; i < len(x); i++ {
        addr := uintptr(unsafe.Pointer(ptr)) + uintptr(i)*size
        elem := (*int)(unsafe.Pointer(addr))
        fmt.Printf("x[%d] = %d\n", i, *elem)
    }
}

💾 Memory Management

Go features automatic memory management with garbage collection, but understanding allocation patterns helps write performant applications.

Stack vs Heap Allocation

Aspect Stack Heap
Allocation Speed Very fast (stack pointer move) Slower (memory manager involved)
Deallocation Automatic (function return) Garbage collected
Size Limits Limited (typically 2-8 MB) Large (system memory)
Access Pattern LIFO (Last In, First Out) Random access
Typical Use Local variables, small structs Escaped variables, large objects

Escape Analysis

How Go Decides Stack vs Heap

The Go compiler performs escape analysis to determine if a variable "escapes" its declaring function:

  • Stays on stack: Variable doesn't outlive the function
  • Escapes to heap: Variable is returned, stored in heap structure, or captured by goroutine
  • Check escape: Use go build -gcflags="-m" to see escape analysis
// Stack vs Heap allocation
func stackAllocation() int {
    x := 42  // Allocated on stack
    return x // Value copied
}

func heapAllocation() *int {
    x := 42  // Escapes to heap
    return &x // Pointer returned
}

// Memory pooling for performance
import "sync"

var pool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

func processData() {
    // Get buffer from pool
    buffer := pool.Get().([]byte)
    defer pool.Put(buffer) // Return to pool
    
    // Use buffer
    // ...
}

// Avoiding memory leaks
type Cache struct {
    mu    sync.RWMutex
    items map[string]*Item
}

type Item struct {
    Value     string
    ExpiresAt time.Time
}

func (c *Cache) Cleanup() {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    now := time.Now()
    for key, item := range c.items {
        if item.ExpiresAt.Before(now) {
            delete(c.items, key) // Remove expired items
        }
    }
}

// Weak references pattern
type WeakRef struct {
    ptr uintptr
}

func NewWeakRef(v interface{}) WeakRef {
    return WeakRef{
        ptr: reflect.ValueOf(v).Pointer(),
    }
}

func (w WeakRef) Get() interface{} {
    if w.ptr == 0 {
        return nil
    }
    // Note: This is unsafe and for demonstration only
    return nil
}

🎨 Advanced Pointer Patterns

Master common pointer patterns and idioms used in production Go code.

Design Patterns with Pointers

Optional Values Pattern

Use pointers to represent optional fields where nil indicates "not set".

type Config struct {
    Timeout *time.Duration // nil = default
    MaxConn *int          // nil = unlimited
}

Method Receivers

Choose between value and pointer receivers based on mutation needs and size.

// Pointer receiver for mutation
func (u *User) UpdateName(name string) {
    u.Name = name
}

Builder Pattern

Chain methods using pointer receivers for fluent API design.

func (b *Builder) WithTimeout(t time.Duration) *Builder {
    b.timeout = t
    return b
}

Copy-on-Write

Share data with pointers, clone when modifications needed.

func (d *Doc) Clone() *Doc {
    copy := *d
    return ©
}
// Optional values with pointers
type Config struct {
    Host     *string  // nil means use default
    Port     *int     // nil means use default
    Timeout  *time.Duration
}

func NewConfig() *Config {
    return &Config{
        Host: StringPtr("localhost"),
        Port: IntPtr(8080),
    }
}

// Helper functions for pointer creation
func StringPtr(s string) *string { return &s }
func IntPtr(i int) *int { return &i }
func BoolPtr(b bool) *bool { return &b }

// Builder pattern with method chaining
type ServerBuilder struct {
    server *Server
}

func NewServerBuilder() *ServerBuilder {
    return &ServerBuilder{
        server: &Server{},
    }
}

func (b *ServerBuilder) WithHost(host string) *ServerBuilder {
    b.server.Host = host
    return b
}

func (b *ServerBuilder) WithPort(port int) *ServerBuilder {
    b.server.Port = port
    return b
}

func (b *ServerBuilder) Build() *Server {
    return b.server
}

// Copy-on-write pattern
type Document struct {
    mu       sync.RWMutex
    content  string
    modified bool
}

func (d *Document) Read() string {
    d.mu.RLock()
    defer d.mu.RUnlock()
    return d.content
}

func (d *Document) Write(content string) {
    d.mu.Lock()
    defer d.mu.Unlock()
    d.content = content
    d.modified = true
}

func (d *Document) Clone() *Document {
    d.mu.RLock()
    defer d.mu.RUnlock()
    return &Document{
        content: d.content,
    }
}

✅ Best Practices and Common Pitfalls

Pointer Best Practices

  • Use pointers for large structs to avoid copying
  • Use pointers when you need to modify the receiver
  • Return pointers from constructors for consistency
  • Always check for nil before dereferencing
  • Use value types for small, immutable data
  • Be consistent with pointer vs value receivers

Common Pointer Pitfalls

  • Nil pointer dereference: Always check for nil
  • Returning pointer to local variable: Go handles this, but be aware
  • Pointer in range loops: Loop variable address changes
  • Interface with nil pointer: Not equal to nil interface

Common Pitfalls with Solutions

// ❌ PITFALL 1: Loop variable pointer
func badPointers() []*int {
    var result []*int
    for i := 0; i < 3; i++ {
        result = append(result, &i) // BUG: all pointers reference same variable!
    }
    return result // All elements point to same address
}

// ✅ SOLUTION: Create new variable in loop
func goodPointers() []*int {
    var result []*int
    for i := 0; i < 3; i++ {
        val := i // Create new variable each iteration
        result = append(result, &val)
    }
    return result
}

// ❌ PITFALL 2: Nil interface vs nil pointer
func nilConfusion() {
    var p *int = nil
    var i interface{} = p
    
    fmt.Println(p == nil)  // true
    fmt.Println(i == nil)  // false! Interface has type info
}

// ❌ PITFALL 3: Returning pointer to local array
func returnArrayPointer() *[3]int {
    arr := [3]int{1, 2, 3}
    return &arr // Go handles this correctly (escapes to heap)
    // But be aware of the performance implications
}

⚡ Performance Considerations

When to Use Pointers

Use Pointers When Use Values When
Struct is large (> 64 bytes typically) Struct is small (few fields)
Need to modify the receiver Data is immutable
Sharing data between goroutines Data is local to function
Implementing optional fields Zero value is meaningful
Building linked data structures Simple scalar types

Performance Tips

  • Benchmark: Use go test -bench to measure pointer vs value performance
  • Profile: Use go tool pprof to identify allocation hotspots
  • Escape analysis: Use -gcflags="-m" to see what escapes to heap
  • Pool objects: Use sync.Pool for frequently allocated objects
  • Preallocate: Size slices correctly to avoid reallocation

🏋️ Practice Exercises

Challenge 1: Linked List

Implement a singly linked list with the following methods:

  • Append(value int) - Add to end
  • Prepend(value int) - Add to beginning
  • Delete(value int) - Remove first occurrence
  • Display() - Print all values

Challenge 2: Binary Tree

Create a binary search tree with pointer-based nodes:

  • Insert(value int) - Add value maintaining BST property
  • Search(value int) bool - Check if value exists
  • InOrder() []int - Return sorted values

Challenge 3: Memory Pool

Implement an object pool to reuse allocated memory:

  • Get() *Object - Retrieve object from pool or allocate new
  • Put(obj *Object) - Return object to pool
  • Track allocation statistics

Challenge 4: Escape Analysis

Write functions that demonstrate:

  • Variables that stay on stack
  • Variables that escape to heap
  • Use compiler flags to verify your predictions