When a copy isn't enough
You write a function to update a user's session token. You pass the session struct, the function assigns a new token, but when you check the session back in your main loop, it is still the old one. The function worked on a copy. Go solves this by giving you pointers. Pointers let you hand a function the exact location of your data instead of a duplicate. They are the bridge between isolated scopes and shared state.
Pointers are addresses, not values
A pointer is not a value. It is a memory address. Think of a variable as a labeled locker in a gym. The locker holds your gear. A pointer is a keycard that says which locker to open. If you hand someone a keycard, they can look inside or swap out your gear without moving the locker itself. In Go, *T means a pointer to type T. The & operator grabs the address of a variable. The * operator follows the address to read or write the actual data.
Go separates the concept of storage from the concept of access. When you declare x := 42, the compiler reserves a slot in memory and writes 42 into it. When you write ptr := &x, you are not copying the number. You are recording the slot's coordinate. Every subsequent use of *ptr tells the runtime to travel to that coordinate and interact with whatever lives there.
Minimal pointer workflow
Here's the simplest pointer workflow: take an address, store it, and use the pointer to read or write the original value.
package main
import "fmt"
func main() {
// x holds the actual integer value.
x := 42
// &x returns the memory address where x lives.
// ptr stores that address, not the number itself.
ptr := &x
// *ptr follows the address to read the underlying value.
fmt.Println(*ptr)
// *ptr on the left side of an assignment writes through the address.
// This mutates the original x without copying it.
*ptr = 100
// x reflects the change because both names target the same slot.
fmt.Println(x)
}
Pointers point. Values hold.
What happens under the hood
When the compiler sees &x, it calculates the exact memory offset where x lives. ptr holds that offset. When you use *ptr, the runtime loads the address from ptr, then reads or writes the memory at that location. Go handles the indirection transparently. You never see raw hex addresses in your code. The language abstracts the hardware details while preserving the performance benefits of direct memory access.
Go also decides where variables live through escape analysis. When you take the address of a local variable, the compiler traces every path the pointer might travel. If the pointer stays inside the function, the variable stays on the stack. Stack allocation is extremely fast because it just moves a pointer register. If the pointer escapes to a return value, a global variable, or a goroutine, the compiler moves the variable to the heap automatically. Heap allocation requires the garbage collector to track the memory, which adds slight overhead. You do not control this decision. You do not need to. The compiler guarantees the memory lives as long as the pointer references it, and it optimizes placement for speed.
Escape analysis is automatic. Trust the compiler.
Why Go pointers are different
Go pointers are safer than pointers in C or C++. You cannot perform pointer arithmetic. You cannot add numbers to a pointer to walk through an array. You cannot cast a pointer to an integer. You cannot compare pointers to check ordering. Go pointers are just references to variables.
This restriction prevents buffer overflows, memory corruption, and undefined behavior. It also simplifies the compiler's job and makes garbage collection more efficient. The garbage collector only needs to track pointers to valid Go objects. It does not need to scan arbitrary integers hoping they might be addresses. You get the benefits of references without the dangers of raw memory manipulation. The language forces you to work with types, not bytes.
No arithmetic. No casting. Just references.
Pointers in real code
Pointers shine when functions need to modify data or when passing a large struct by value would waste memory. This example shows a function updating a struct field via a pointer, mimicking a real configuration reload pattern.
package main
import "fmt"
// Config holds application settings that change at runtime.
type Config struct {
MaxRetries int
Timeout string
}
// UpdateConfig modifies the configuration in place.
// It takes a pointer so changes persist after the call returns.
func UpdateConfig(c *Config, retries int) {
// Direct field access works because c is a pointer to Config.
// Go automatically dereferences pointers for struct field access.
c.MaxRetries = retries
}
func main() {
// Create a config instance.
app := Config{MaxRetries: 3, Timeout: "5s"}
// Pass the address of app to UpdateConfig.
// The function receives a pointer to the original struct.
UpdateConfig(&app, 5)
// The value changed because UpdateConfig modified the original struct.
fmt.Println(app.MaxRetries)
}
Go automatically dereferences pointers when you access struct fields. Writing c.MaxRetries is shorthand for (*c).MaxRetries. The language makes this ergonomic so you do not clutter your code with parentheses. When defining methods on a pointer receiver, the community convention is to use a short receiver name that matches the type. You will see (c *Config) everywhere. You will rarely see (self *Config) or (this *Config). Keep it short. It reads faster.
Pass pointers to mutate. Pass values to protect.
Pitfalls and compiler rules
The most common runtime crash is dereferencing a nil pointer. A pointer's zero value is nil, which means it points to nothing. Accessing *ptr when ptr is nil causes a panic with runtime error: invalid memory address or nil pointer dereference. Always check for nil if the pointer might be uninitialized.
if ptr == nil {
// Handle the missing value.
return
}
The compiler enforces type safety strictly. If you pass a pointer where a value is expected, the compiler rejects it with cannot use ptr (type *int) as type int in argument. Go does not implicitly convert pointers to values. You must dereference explicitly. This strictness prevents accidental aliasing bugs where a function silently modifies data it was supposed to read.
Go also has a strong convention about pointers to strings. Avoid *string for simple data. Strings are immutable and cheap to pass by value. Passing a pointer to a string adds indirection without benefit. Use *string only when you need to represent a missing value distinct from an empty string, or when an external API requires it. The same rule applies to *int and *bool in most application code. Reserve pointer types for structs, interfaces, and types that must be shared or mutated.
Nil checks save panics. Check before dereference.
When to use pointers
Use a pointer when a function needs to modify the caller's variable. Use a pointer when passing a large struct to avoid copying the entire value. Use a pointer when the type contains a mutex or other reference that must not be copied. Use a value when the data is small and immutable, like an integer or a short string. Use a value when you want to guarantee the caller cannot change the data. Use a value when the type is a map, slice, or channel, since these are already reference types.
Small data by value. Large data by pointer.