3.5 KiB
Pointer Types Deep Dive
Regular Pointers (*T)
Stack vs Heap (Escape Analysis)
Go's compiler decides whether to allocate on the stack or heap. A variable "escapes" to the heap when its lifetime extends beyond the function:
func noEscape() int {
x := 42
return x // x stays on stack — copied on return
}
func escapes() *int {
x := 42
return &x // x escapes to heap — pointer outlives function
}
Use go build -gcflags="-m" to see escape analysis decisions. Heap allocations add GC pressure — avoid unnecessary escapes in hot paths.
new(T) vs &T{}
Both allocate and return a pointer. &T{} is preferred because it allows field initialization:
p := new(Point) // *Point with zero values
p := &Point{X: 1} // *Point with initialized fields — preferred
unsafe.Pointer
unsafe.Pointer bypasses Go's type system for FFI and low-level memory manipulation. Only the 6 patterns from the Go spec are safe; any other pattern is undefined behavior.
The 6 Valid Patterns (from the Go spec)
These are the ONLY safe ways to use unsafe.Pointer. Any other pattern is undefined behavior.
Pattern 1: Convert *T to *U via unsafe.Pointer
// Reinterpret a float64 as its raw bits
f := 1.5
bits := *(*uint64)(unsafe.Pointer(&f))
Pattern 2: Convert unsafe.Pointer to uintptr and back (same expression)
// Pointer arithmetic — MUST be a single expression
p := unsafe.Pointer(uintptr(unsafe.Pointer(&s.field)) + offset)
Pattern 3: reflect.Value.Pointer() or UnsafeAddr() to unsafe.Pointer
p := unsafe.Pointer(reflect.ValueOf(&x).Pointer())
Pattern 4: syscall.Syscall arguments
syscall.Syscall(SYS_READ, fd, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
Critical Rule: NEVER Store uintptr Across Statements
// ✗ DANGEROUS — GC can move the object between these two lines
u := uintptr(unsafe.Pointer(&x))
// ... GC may run here, moving x ...
p := unsafe.Pointer(u) // dangling pointer
// ✓ Safe — single expression
p := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + offset)
Modern Alternatives (prefer these)
| Function | Since | Purpose |
|---|---|---|
unsafe.Add(ptr, len) |
Go 1.17 | Pointer arithmetic without uintptr conversion |
unsafe.Slice(ptr, len) |
Go 1.17 | Create slice from pointer + length |
unsafe.String(ptr, len) |
Go 1.20 | Create string from pointer + length |
unsafe.SliceData(s) |
Go 1.17 | Get pointer to slice's backing array |
unsafe.StringData(s) |
Go 1.20 | Get pointer to string's backing array |
These are safer than manual uintptr arithmetic because they keep values as pointers (visible to GC) throughout.
weak.Pointer[T] (Go 1.24+)
A weak pointer holds a reference to an object without preventing garbage collection. When the GC reclaims the object, Value() returns nil.
strong := new(MyType)
w := weak.Make(strong)
if p := w.Value(); p != nil {
// object still alive
} else {
// object was garbage collected
}
Use Cases
- Deduplication caches — intern equivalent values without preventing GC
- Automatic cache eviction — cached objects evict when no strong references remain
runtime.AddCleanup vs runtime.SetFinalizer
Prefer runtime.AddCleanup (Go 1.24+) over runtime.SetFinalizer:
- Multiple cleanups can be registered per object
- Cleanup function receives a value, not a pointer to the collected object
- No risk of resurrecting the object
- Works correctly with weak pointers