🎯 Chapter 12 of 20

Pointers

Go pointers are safer than C pointers β€” no pointer arithmetic, no manual memory management, and a garbage collector handles memory. But understanding how pointers work is essential for writing efficient, correct Go programs.

⏱ β€”πŸ“Š Intermediate

🎯 Learning Objectives

βœ… After this chapter you will:
  • Use the address-of (&) and dereference (*) operators
  • Understand pointer types and nil pointers
  • Know why Go has no pointer arithmetic (and how unsafe bypasses this)
  • Use new() to allocate zeroed values
  • Understand pointers vs value semantics in function calls
  • Work with pointers to structs correctly
  • Explain when Go uses pointers under the hood (slices, maps, channels)

πŸ“ Pointer Basics

A pointer is a variable that stores the memory address of another variable. In Go, &x gives the address of x, and *p gives the value that pointer p points to.

πŸ“Š Pointer Memory Layout
0x100042← x (int, value 42)
0x10080x1000← p (*int, stores address of x)
p == &x == 0x1000 | *p == 42 | **(&p) == 42
go
package main

import "fmt"

func main() {
    x := 42

    // & β€” address-of operator: gives the address of x
    p := &x            // p is *int, holds address of x
    fmt.Println("x =", x)      // 42
    fmt.Println("p =", p)      // 0xc000016090 (some address)
    fmt.Println("*p =", *p)    // 42 (dereference: follow the pointer)

    // Modifying through pointer β€” modifies the original!
    *p = 100
    fmt.Println("x after *p=100:", x)  // 100

    // Pointer type syntax: *T
    var q *int         // q is a *int, zero value is nil
    fmt.Println("q ==", q)  // 

    // nil pointer dereference β€” PANIC!
    // fmt.Println(*q)  // ← panic: runtime error: nil pointer dereference

    // Always check for nil before dereferencing
    if q != nil {
        fmt.Println(*q)
    } else {
        fmt.Println("q is nil β€” safe check")
    }

    // Pointer to pointer
    pp := &p           // pp is **int
    fmt.Println("**pp =", **pp)  // 100

    // new() allocates a zeroed value and returns a pointer to it
    n := new(int)      // *int, zero value = 0
    *n = 99
    fmt.Println("n =", n, "*n =", *n)

    s := new(string)   // *string, zero value = ""
    *s = "hello"
    fmt.Println("*s =", *s)
}
βœ… Output
x = 42 p = 0xc000016090 *p = 42 x after *p=100: 100 q = <nil> q is nil β€” safe check **pp = 100 n = 0xc000018098 *n = 99 *s = hello

πŸ”„ Pointers in Functions

Go is pass-by-value. To modify a caller's variable inside a function, pass a pointer.

go
package main

import "fmt"

// ❌ No effect β€” modifies a local copy
func badIncrement(n int) {
    n++
}

// βœ… Modifies the caller's variable
func increment(n *int) {
    *n++
}

// Swap two integers via pointers
func swap(a, b *int) {
    *a, *b = *b, *a
}

// Parse config: uses pointer to signal "not provided" vs "zero value"
func parseTimeout(timeout *int) int {
    if timeout == nil {
        return 30  // default
    }
    return *timeout
}

func main() {
    x := 5

    badIncrement(x)
    fmt.Println("after badIncrement:", x)  // 5 β€” unchanged

    increment(&x)
    fmt.Println("after increment:", x)  // 6 β€” changed!

    a, b := 10, 20
    swap(&a, &b)
    fmt.Println("after swap:", a, b)  // 20 10

    // Pointer as "optional" value
    zero := 0
    fmt.Println("nil timeout:", parseTimeout(nil))    // 30 (default)
    fmt.Println("zero timeout:", parseTimeout(&zero)) // 0 (explicit zero)
    timeout := 60
    fmt.Println("60s timeout:", parseTimeout(&timeout)) // 60

    // Pointer to struct β€” auto-dereferences field access
    type Point struct{ X, Y int }
    p := &Point{X: 1, Y: 2}
    fmt.Println(p.X)    // 1 (Go auto-dereferences: (*p).X)
    p.X = 10            // (*p).X = 10
    fmt.Println(*p)     // {10 2}
}
βœ… Output
after badIncrement: 5 after increment: 6 after swap: 20 10 nil timeout: 30 zero timeout: 0 60s timeout: 60 1 {10 2}

βš–οΈ Go Pointers vs C Pointers

FeatureGoC
Pointer arithmetic (p++)❌ Not allowedβœ… Allowed
Manual memory management❌ GC handles itβœ… malloc/free
Null pointer dereferencePanic (safe crash)Undefined behavior (crash or data corruption)
Dangling pointers❌ Impossible (GC tracks references)βœ… Possible (use-after-free)
Buffer overflows via pointer❌ Bounds-checkedβœ… Possible
unsafe.Pointerβœ… Escape hatch (explicit, auditable)N/A
go
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // No pointer arithmetic in safe Go:
    x := [5]int{1, 2, 3, 4, 5}
    p := &x[0]
    // p++  // ← compile error: invalid operation (no pointer arithmetic)

    // But unsafe.Pointer allows it (use with extreme caution!):
    uptr := unsafe.Pointer(p)  // convert to unsafe.Pointer
    p2 := (*int)(unsafe.Pointer(uintptr(uptr) + unsafe.Sizeof(x[0])))
    fmt.Println("x[1] via unsafe:", *p2)  // 2

    // This is dangerous! Use only for: C interop, optimised data structures, reflection
    // Never use in normal application code

    // new() vs make()
    // new(T): allocates a *T, zero-initialised β€” for value types
    pi := new(int)   // *int, value = 0
    ps := new(string) // *string, value = ""
    fmt.Printf("pi=%p *pi=%d\n", pi, *pi)
    fmt.Printf("ps=%p *ps=%q\n", ps, *ps)

    // make(T, args): for slices, maps, channels β€” these need initialisation
    // slice := new([]int) β€” creates *[]int with nil slice inside!
    slice := make([]int, 5)  // creates properly initialised slice
    fmt.Println("slice:", slice)
}
βœ… Output
x[1] via unsafe: 2 pi=0xc000018090 *pi=0 ps=0xc000070020 *ps="" slice: [0 0 0 0 0]

🎯 When to Use Pointers

Use Pointer WhenUse Value When
Function needs to modify the argumentFunction only reads the argument
Struct is large (avoid expensive copy)Struct is small (1-3 words)
Type implements pointer receiver methodsType uses value receivers only
nil is a meaningful "not set" valueZero value is meaningful
Struct has a sync.Mutex or similar (must not copy!)Small immutable data (Point, Color)
Allocating a struct and returning it from a constructorReturning primitive values (int, bool)

🎀 Interview Questions

1
What is the difference between new(T) and make(T, ...)?
β–Ά
Detailed Answer

new(T): allocates memory for a value of type T, zero-initialises it, and returns a pointer *T. Works for any type. Used for structs, primitives, and arrays. new(int) returns a *int pointing to a zeroed int.

make(T, args): only works for slice, map, and channel. Returns an initialised (not zero-value) instance of T β€” not a pointer. Maps and slices need internal initialisation before they're safe to use; make handles this. make([]int, 5) returns a []int with 5 zeroed elements and a valid header.

Common mistake: Using new([]int) β€” this creates a *[]int pointing to a nil slice, which is rarely what you want. Use make([]int, len) instead.

2
Why doesn't Go have pointer arithmetic?
β–Ά
Detailed Answer

Pointer arithmetic allows computing arbitrary memory addresses, which enables: buffer overflows, use-after-free bugs, type confusion attacks, and reading/writing arbitrary memory. These are entire classes of security vulnerabilities and crashes in C/C++.

Go eliminated pointer arithmetic to make the language memory-safe by default. The GC also relies on tracking all pointers β€” arbitrary pointer arithmetic would break this tracking.

When you genuinely need pointer arithmetic (C interop, low-level system code), Go provides unsafe.Pointer and uintptr as an explicit escape hatch. The name "unsafe" signals that you've left the safety guarantees of the language.

πŸ§ͺ Quiz

🧩
Pointers
4 questions
1. What does p := new(int) return?
2. What happens when you dereference a nil pointer (*p where p == nil)?
3. Is pointer arithmetic (p++) allowed in Go?
4. For p := &Point{X:1}; p.X = 10 β€” what is the equivalent explicit form?
β€”

πŸ“Œ Cheat Sheet

πŸ“Œ Pointers β€” Quick Reference
Address-of
&x β†’ *T (pointer to x)
Dereference
*p β†’ T (value at pointer)
Nil pointer
var p *T // zero value = nil
Nil check
if p != nil { *p }
Allocate
p := new(T) // *T, zero value
Struct field
p.Field == (*p).Field
Modify via ptr
func f(p *T) { *p = newVal }
No arithmetic
p++ is ILLEGAL in Go
Optional value
*int = nil means "not set"
Unsafe
unsafe.Pointer for low-level ops