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.
π― Learning Objectives
- 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.
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)
} π Pointers in Functions
Go is pass-by-value. To modify a caller's variable inside a function, pass a pointer.
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}
}βοΈ Go Pointers vs C Pointers
| Feature | Go | C |
|---|---|---|
Pointer arithmetic (p++) | β Not allowed | β Allowed |
| Manual memory management | β GC handles it | β malloc/free |
| Null pointer dereference | Panic (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 |
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)
}π― When to Use Pointers
| Use Pointer When | Use Value When |
|---|---|
| Function needs to modify the argument | Function only reads the argument |
| Struct is large (avoid expensive copy) | Struct is small (1-3 words) |
| Type implements pointer receiver methods | Type uses value receivers only |
| nil is a meaningful "not set" value | Zero 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 constructor | Returning primitive values (int, bool) |
π€ Interview Questions
new(T) and make(T, ...)?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.
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
p := new(int) return?*p where p == nil)?p++) allowed in Go?p := &Point{X:1}; p.X = 10 β what is the equivalent explicit form?π Cheat Sheet
&x β *T (pointer to x)*p β T (value at pointer)var p *T // zero value = nilif p != nil { *p }p := new(T) // *T, zero valuep.Field == (*p).Fieldfunc f(p *T) { *p = newVal }p++ is ILLEGAL in Go*int = nil means "not set"unsafe.Pointer for low-level ops