Go Pointers Basics — Middle Level¶
1. Introduction¶
At the middle level, you reason precisely about pointers as typed memory addresses with escape analysis: when they're stack vs heap, when to use them vs values, the safety they provide vs the complexity they add, and the patterns they enable (sharing, optional values, linked structures).
2. Prerequisites¶
- Junior-level pointers material
- Call by value (2.6.7)
- Basic understanding of memory and the stack/heap distinction
3. Glossary¶
| Term | Definition |
|---|---|
| Escape | When a variable's lifetime exceeds its enclosing function |
| Stack allocation | Variable stored in the goroutine stack frame |
| Heap allocation | Variable stored on the GC-managed heap |
| Aliasing | Multiple pointers/references to the same memory |
unsafe.Pointer | Untyped pointer for low-level operations |
uintptr | Integer type capable of holding a pointer value |
| Pointer receiver | Method receiver of pointer type (*T) |
| Constructor | Function returning a pointer to a freshly-allocated value |
4. Core Concepts¶
4.1 Pointers and Escape Analysis¶
The compiler decides per-allocation: - If a pointer to a local doesn't escape, the local stays on the stack. - If it escapes (returned, stored in global, captured by escaping closure), heap-allocated.
func stays() int {
n := 5
p := &n // p doesn't escape
return *p // n stays on stack
}
func escapes() *int {
n := 5
return &n // n escapes; allocated on heap
}
Verify with go build -gcflags="-m".
4.2 Pointer to Variable vs Pointer to Composite Literal¶
n := 5
p1 := &n // p1 points to n on the stack (or heap if n escapes)
p2 := &Point{X: 1} // composite literal allocates a new Point; p2 points to it
Both are addressable expressions; both yield valid pointers.
4.3 Pointer Receivers vs Value Receivers¶
type Counter struct{ n int }
func (c Counter) Show() { fmt.Println(c.n) } // value receiver — copy
func (c *Counter) Inc() { c.n++ } // pointer receiver — original
Choose pointer receivers when: - The method mutates the receiver. - The receiver is large (avoid copy). - For consistency with other methods on the type.
Choose value receivers when: - The method only reads. - The type is small. - Immutability is desired.
4.4 Method Set Rules¶
For interface satisfaction, the method set depends on receiver type:
| Receiver | Available on |
|---|---|
func (T) | T and *T |
func (*T) | *T only (not on bare T) |
So if you have a value t T and an interface requires methods with *T receivers, you must use &t.
4.5 Pointer Comparison¶
p1 := new(int)
p2 := new(int)
p3 := p1
fmt.Println(p1 == p2) // false (different addresses)
fmt.Println(p1 == p3) // true (same address)
fmt.Println(p1 == nil) // false
Pointers are comparable; equality is address equality.
4.6 unsafe.Pointer Bridge¶
import "unsafe"
x := int64(42)
p := &x
up := unsafe.Pointer(p) // *int64 → unsafe.Pointer
up2 := unsafe.Pointer(uintptr(up) + 4) // arithmetic via uintptr (DANGEROUS)
unsafe.Pointer allows casting between any pointer types and uintptr. Use only for low-level interop with C, runtime, or specific tricks. Most code should never use it.
5. Real-World Analogies¶
A real-estate broker's listing: the listing has a property address. The broker can pass the address to multiple agents. They all reach the same property. Modifying the property (renovating) is visible through any agent's reference.
A library card: the card grants access to a shared book. Multiple cards (pointers) can access the same book. Damaging the book affects everyone.
6. Mental Models¶
Model 1 — Pointer as Address + Type¶
*int = "pointer to a memory location holding an int"
*Point = "pointer to a memory location holding a Point struct"
Each pointer carries: - The address (8 B on 64-bit). - The type (compile-time only; doesn't take runtime space).
Model 2 — Stack vs Heap Decision¶
Compile time:
Does the pointer escape?
- returned from function?
- stored in global?
- captured by escaping closure?
→ If yes: heap.
→ If no: stack.
Runtime:
Stack: freed at function return.
Heap: tracked by GC, freed when no pointers remain.
7. Pros & Cons¶
Pros¶
- Mutation through function calls
- Sharing without copying
- Optional values via nil
- Linked data structures
- Method-receiver semantics
Cons¶
- Nil dereference panics
- Aliasing bugs (concurrent or sequential)
- Indirection cost (small)
- Heap allocation when escaping (GC pressure)
8. Use Cases¶
- Mutator functions and methods.
- Constructors returning new objects.
- Optional fields (nil = absent).
- Linked lists, trees, graphs.
- Caching/sharing expensive objects.
- Method values bound to receivers.
9. Code Examples¶
Example 1 — Constructor Pattern¶
package main
import "fmt"
type Server struct {
Addr string
Port int
}
func NewServer(addr string, port int) *Server {
return &Server{Addr: addr, Port: port}
}
func main() {
s := NewServer("localhost", 8080)
fmt.Println(s)
}
Example 2 — Optional Field via Pointer¶
package main
import "fmt"
type Config struct {
Timeout *int // nil means "use default"
}
func handle(c *Config) {
timeout := 30
if c.Timeout != nil {
timeout = *c.Timeout
}
fmt.Println("using timeout:", timeout)
}
func main() {
handle(&Config{}) // 30
t := 60
handle(&Config{Timeout: &t}) // 60
}
Example 3 — Linked List¶
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func print(n *Node) {
for ; n != nil; n = n.Next {
fmt.Print(n.Value, " ")
}
fmt.Println()
}
func main() {
head := &Node{Value: 1, Next: &Node{Value: 2, Next: &Node{Value: 3}}}
print(head) // 1 2 3
}
Example 4 — Pointer Receivers Throughout¶
package main
import "fmt"
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func (c *Counter) Get() int { return c.n }
func (c *Counter) Reset() { c.n = 0 }
func main() {
c := &Counter{}
c.Inc(); c.Inc(); c.Inc()
fmt.Println(c.Get()) // 3
c.Reset()
fmt.Println(c.Get()) // 0
}
Example 5 — Pointer in Interface¶
package main
import "fmt"
type Shape interface{ Area() float64 }
type Circle struct{ R float64 }
func (c *Circle) Area() float64 { return 3.14 * c.R * c.R }
func main() {
var s Shape = &Circle{R: 2}
fmt.Println(s.Area()) // 12.56
}
*Circle satisfies Shape because of the pointer receiver. Circle{} value would NOT satisfy unless the method had a value receiver.
Example 6 — Pointer Helper for Optionals¶
package main
import "fmt"
func ptrTo[T any](v T) *T {
return &v
}
type Settings struct {
Enabled *bool
Name *string
}
func main() {
s := Settings{
Enabled: ptrTo(true),
Name: ptrTo("ada"),
}
fmt.Printf("enabled=%v name=%v\n", *s.Enabled, *s.Name)
}
10. Coding Patterns¶
Pattern 1 — Constructor¶
Pattern 2 — Builder Returning *T¶
Pattern 3 — Optional Pointer Field¶
Pattern 4 — Mutating Helper¶
Pattern 5 — Pointer to Pointer (rare)¶
11. Clean Code Guidelines¶
- Use pointers consistently for a type that has any pointer-receiver method.
- Document what nil means for optional pointer fields.
- Return pointers from constructors (
func NewT() *T). - Avoid
**Tunless absolutely necessary. - Nil-check at API boundaries.
- Prefer composite literals +
&overnewwhen initializing.
12. Product Use / Feature Example¶
A pluggable handler with optional configuration:
package main
import "fmt"
type Config struct {
Verbose bool
Logger *Logger // optional; nil disables logging
}
type Logger struct {
Prefix string
}
func (l *Logger) Log(msg string) {
fmt.Println("[" + l.Prefix + "]", msg)
}
func handle(cfg *Config) {
if cfg.Verbose {
fmt.Println("verbose mode on")
}
if cfg.Logger != nil {
cfg.Logger.Log("handled")
}
}
func main() {
cfg := &Config{Verbose: true, Logger: &Logger{Prefix: "APP"}}
handle(cfg)
}
The pointer to Logger is optional; nil bypasses logging.
13. Error Handling¶
func get(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("invalid id")
}
u, ok := users[id]
if !ok {
return nil, fmt.Errorf("not found")
}
return u, nil
}
u, err := get(1)
if err != nil { return err }
// safe: u is non-nil
14. Security Considerations¶
- Nil pointers from external data can crash your program.
- Returning pointers to internal mutable state lets callers modify it.
- Aliased pointers across goroutines require synchronization.
- Wipe sensitive data through pointers when done (
*p = SecretType{}).
15. Performance Tips¶
- Pointer pass: 8 B. Free.
- Value pass small types: free (registers).
- Value pass large types: expensive (memcpy).
- Pointer indirection: 1-2 cycles per dereference.
- Heap allocation cost: ~25 ns + GC tracking.
- Stack allocation: free.
For hot paths, prefer values when small, pointers when large; verify with profile.
16. Metrics & Analytics¶
type Metric struct {
Name string
Tags []string
Value float64
}
func emit(m *Metric) {
if m == nil { return }
fmt.Printf("[%s] %v %f\n", m.Name, m.Tags, m.Value)
}
17. Best Practices¶
- Use pointer receivers consistently per type.
- Use constructors returning pointers.
- Document nil semantics for optional fields.
- Always nil-check at API boundaries.
- Avoid
**Tandunsafe.Pointer. - Prefer
&T{...}overnew(T)when initializing fields. - Verify escape behavior with
-gcflags="-m".
18. Edge Cases & Pitfalls¶
Pitfall 1 — Nil Receiver Method Call¶
type T struct{ n int }
func (t *T) Inc() { t.n++ }
var t *T
t.Inc() // panic: nil pointer dereference
Pitfall 2 — Returning Pointer to Local¶
Generally safe (escape analysis), but the local moves to the heap. Be aware:
Pitfall 3 — Pointer Receiver on Non-Addressable Value¶
m := map[string]Counter{"a": {n: 1}}
// m["a"].Inc() // compile error: cannot call pointer method on m["a"]
Pitfall 4 — Address of Loop Variable (Pre 1.22)¶
Pitfall 5 — Aliasing¶
19. Common Mistakes¶
| Mistake | Fix |
|---|---|
| Forgetting nil check before deref | Add if p != nil |
| Mixing value and pointer receivers | Be consistent per type |
| Calling pointer method on non-addressable value | Store pointer or extract |
| Pre-1.22 loop var pointer capture | Shadow or upgrade |
| Returning unsafe pointer to internal state | Defensive copy |
20. Common Misconceptions¶
Misconception 1: "Pointers are unsafe like in C." Truth: Go's pointers are typed, no arithmetic, GC-managed. Safe to return from functions.
Misconception 2: "Pointers always heap-allocate." Truth: Only when they escape. Local pointers stay on the stack.
Misconception 3: "I should use pointers everywhere for performance." Truth: For small types, value pass is faster.
Misconception 4: "new(T) is rarely useful." Truth: It's idiomatic for zero-initialized values; &T{} for initialized.
Misconception 5: "Comparing pointers is the same as reflect.DeepEqual." Truth: == compares addresses. To compare pointed-to values, dereference: *p1 == *p2.
21. Tricky Points¶
- Pointer receiver methods can be called on values IF the value is addressable.
- Method set:
Thas T-receiver methods;*Thas both T- and *T-receiver methods. - Pointer-to-pointer is rare but legal for "modify the pointer itself" semantics.
&T{}andnew(T)are equivalent for zero-init.unsafe.Pointerdefeats the type system; use sparingly.
22. Test¶
package main
import "testing"
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func TestNilCounter(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("expected panic on nil")
}
}()
var c *Counter
c.Inc()
}
func TestCounter(t *testing.T) {
c := &Counter{}
c.Inc()
c.Inc()
if c.n != 2 {
t.Errorf("got %d, want 2", c.n)
}
}
23. Tricky Questions¶
Q1: What does this print?
A:2. t is addressable (local variable); Go automatically uses &t. Mutation persists. Q2: Will this compile?
A: No. The result of a function call is not addressable; you can't take&get().Inc(). Workaround: assign to variable first. Q3: What does this print?
A:false true. Different addresses, both zero values. 24. Cheat Sheet¶
// Type
var p *T
// Allocate
p := new(T)
p := &T{...}
// Take address
p := &x
// Dereference
v := *p
*p = newValue
// Nil check
if p != nil { use(p) }
// Method receiver
func (t *T) Mutate() { t.field = ... }
func (t T) Read() int { return t.field }
// Pointer in interface
var i Interface = &Concrete{}
// Optional (helper)
func ptrTo[T any](v T) *T { return &v }
25. Self-Assessment Checklist¶
- I use pointers for mutation
- I check for nil before dereferencing
- I use pointer receivers consistently per type
- I understand escape analysis basics
- I avoid
**Tunless necessary - I use
&T{}for initialized allocation - I document nil semantics for optional fields
- I handle the loop-variable pointer pitfall
26. Summary¶
Pointers are typed addresses for variables. Use them for mutation, sharing, optional values, and linked data structures. The compiler handles allocation via escape analysis: pointers to escaping locals go to the heap. Be consistent with receiver types for a given type. Always nil-check at boundaries. Prefer values for small types; pointers for large ones.
27. What You Can Build¶
- Constructors and factories
- Linked structures (lists, trees)
- Pluggable optional features
- Mutator APIs
- Caches and shared state
- Method-rich types
28. Further Reading¶
- Effective Go — Pointers vs values
- Go Spec — Pointer types
- Dave Cheney — Allocation efficiency
- Go Blog — Profiling Go programs
29. Related Topics¶
- 2.6.7 Call by Value
- 2.7.2 Pointers with Structs
- 2.7.3 With Maps & Slices
- 2.7.4 Memory Management
- Chapter 3 Methods
30. Diagrams & Visual Aids¶
Pointer types¶
type | example | size
----------|-------------|-----
*int | &x for int x| 8 B
*Point | &Point{} | 8 B
*string | &s | 8 B
*[10]int | &arr | 8 B
**int | &p where p *int | 8 B
Method set¶
Type T {
func (t T) M1() -- in method set of T AND *T
func (t *T) M2() -- in method set of *T only
}
To satisfy an interface requiring M2, you must use *T.