Go Pointers: Memory Addresses and Dereferencing
Go is a pass-by-value language. Every time you pass a variable to a function or assign it to another variable, Go creates a copy. For integers and booleans, this is trivial. But for large structs or...
Key Insights
- Pointers in Go store memory addresses rather than values, enabling functions to modify caller data and avoid expensive copies of large structs
- The
&operator gets a variable’s address while*dereferences a pointer to access its value—mixing these up is the most common pointer mistake - Unlike C, Go pointers are safe by design with no pointer arithmetic, automatic garbage collection, and compile-time nil safety checks in many cases
Understanding Pointers and Memory Addresses
Go is a pass-by-value language. Every time you pass a variable to a function or assign it to another variable, Go creates a copy. For integers and booleans, this is trivial. But for large structs or when you need a function to modify the original data, copying becomes problematic.
Pointers solve this by storing memory addresses instead of values. When you pass a pointer, you’re passing a small address (typically 8 bytes on 64-bit systems) rather than the entire data structure.
package main
import "fmt"
func main() {
x := 42
fmt.Printf("Value: %d\n", x)
fmt.Printf("Address: %p\n", &x)
var p *int = &x
fmt.Printf("Pointer value (address): %p\n", p)
fmt.Printf("Pointer's own address: %p\n", &p)
}
This outputs something like:
Value: 42
Address: 0xc0000b4008
Pointer value (address): 0xc0000b4008
Pointer's own address: 0xc0000b4010
The & operator (address-of) gives you the memory location where x lives. The pointer p stores that address. Notice that p itself also has an address—pointers are variables too.
Declaring and Initializing Pointers
Go provides three main ways to work with pointers, each suited for different scenarios.
// Method 1: Declare pointer type, assign address of existing variable
var x int = 100
var p1 *int = &x
// Method 2: Short declaration with address-of operator
y := 200
p2 := &y
// Method 3: Allocate new memory with new()
p3 := new(int)
*p3 = 300
// Zero value of a pointer is nil
var p4 *int
fmt.Println(p4) // <nil>
The new() function allocates memory for a value, initializes it to the zero value of that type, and returns a pointer to it. This is useful when you need a pointer but don’t have an existing variable to reference.
Understanding nil is crucial. A nil pointer doesn’t point anywhere. Attempting to dereference it causes a runtime panic—one of the few ways to crash a Go program.
Dereferencing: Accessing Values Through Pointers
The * operator serves double duty in Go. In type declarations (*int), it denotes a pointer type. In expressions, it dereferences a pointer, accessing the value at that memory address.
func main() {
count := 5
ptr := &count
// Read through pointer
fmt.Println(*ptr) // 5
// Write through pointer
*ptr = 10
fmt.Println(count) // 10 - original variable changed!
// The pointer itself can be reassigned
other := 20
ptr = &other
fmt.Println(*ptr) // 20
}
This demonstrates pointers’ power: modifying *ptr modifies count because they reference the same memory location. This isn’t magic or action-at-a-distance—you’re directly manipulating the memory where count is stored.
Pointers as Function Parameters
This is where pointers become indispensable. Without pointers, functions operate on copies and cannot modify the caller’s data.
type User struct {
Name string
Email string
Age int
}
// Pass by value - modifications don't affect caller
func updateAgeValue(u User, newAge int) {
u.Age = newAge
}
// Pass by pointer - modifications affect caller
func updateAgePointer(u *User, newAge int) {
u.Age = newAge
}
func main() {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
updateAgeValue(user, 31)
fmt.Println(user.Age) // Still 30
updateAgePointer(&user, 31)
fmt.Println(user.Age) // Now 31
}
Beyond modification, pointers improve performance with large structs. Copying a 1KB struct on every function call wastes CPU cycles and stack space. Passing a pointer copies only 8 bytes regardless of struct size.
type LargeConfig struct {
Settings [1000]string
Metadata map[string]interface{}
// ... many more fields
}
// Inefficient: copies entire struct
func ProcessConfigValue(cfg LargeConfig) {
// ...
}
// Efficient: copies only pointer
func ProcessConfigPointer(cfg *LargeConfig) {
// ...
}
Go’s syntax sugar lets you access struct fields through pointers without explicit dereferencing: cfg.Settings works whether cfg is a value or pointer. The compiler handles it.
Common Pointer Patterns and Pitfalls
The most dangerous pitfall is dereferencing nil pointers:
var p *int
// *p = 5 // panic: runtime error: invalid memory address
// Always check for nil before dereferencing
if p != nil {
*p = 5
}
// Or initialize before use
p = new(int)
*p = 5
Pointer receivers on methods are idiomatic Go for types that should be modified:
type Counter struct {
value int
}
// Pointer receiver - modifies the counter
func (c *Counter) Increment() {
c.value++
}
// Value receiver - works on a copy
func (c Counter) Value() int {
return c.value
}
func main() {
counter := Counter{}
counter.Increment()
counter.Increment()
fmt.Println(counter.Value()) // 2
}
Use pointer receivers when:
- The method needs to modify the receiver
- The struct is large (avoid copying)
- Consistency matters (if some methods use pointer receivers, use them for all methods on that type)
Use value receivers when:
- The type is small (primitive types, small structs)
- The method doesn’t modify state
- The type should be immutable
Pointers to pointers are legal but rarely needed:
func main() {
x := 42
p := &x
pp := &p
fmt.Println(**pp) // 42
**pp = 100
fmt.Println(x) // 100
}
This appears in scenarios like modifying a pointer itself within a function, though such designs usually indicate a code smell.
Practical Applications
Pointers are essential for dynamic data structures. Here’s a simple linked list node:
type Node struct {
Value int
Next *Node
}
func (n *Node) Append(value int) {
current := n
for current.Next != nil {
current = current.Next
}
current.Next = &Node{Value: value}
}
func (n *Node) Print() {
current := n
for current != nil {
fmt.Printf("%d -> ", current.Value)
current = current.Next
}
fmt.Println("nil")
}
func main() {
head := &Node{Value: 1}
head.Append(2)
head.Append(3)
head.Print() // 1 -> 2 -> 3 -> nil
}
Without pointers, you couldn’t create the self-referential structure that makes linked lists possible.
Another practical pattern is optional values. Before Go 1.18’s generics, pointer-to-type was the standard way to represent “value or nothing”:
type Config struct {
Timeout *int // nil means use default
MaxRetries *int
}
func NewConfig() *Config {
return &Config{}
}
func (c *Config) GetTimeout() int {
if c.Timeout != nil {
return *c.Timeout
}
return 30 // default
}
func main() {
cfg := NewConfig()
fmt.Println(cfg.GetTimeout()) // 30
timeout := 60
cfg.Timeout = &timeout
fmt.Println(cfg.GetTimeout()) // 60
}
This pattern distinguishes between “not set” (nil) and “set to zero value” (pointer to 0).
Best Practices and Guidelines
Start with values, use pointers when necessary. Go’s compiler is smart about escape analysis—it automatically moves variables to the heap when they need to outlive their stack frame. You don’t need to think about stack vs heap allocation in most cases.
Don’t return pointers to local variables from functions just because you can. The compiler handles this safely, but it forces heap allocation. Return values directly when possible.
// Unnecessary heap allocation
func NewCounter() *int {
x := 0
return &x
}
// Better: return value directly
func NewCounter() int {
return 0
}
Use pointers for slices and maps sparingly. These types are already reference types—they contain pointers internally. Passing them by value is usually fine.
Document whether functions modify their pointer parameters. This isn’t enforced by the compiler, so clear function names and comments matter:
// UpdateUser modifies the user in place
func UpdateUser(u *User) error {
// ...
}
// GetUserCopy returns a copy, original unchanged
func GetUserCopy(u User) User {
// ...
}
Pointers are fundamental to Go, but they’re simpler than in C or C++. No pointer arithmetic, no manual memory management, no dangling pointers. Master the & and * operators, understand when to use pointer receivers, and you’ll write idiomatic Go that’s both efficient and safe.