Address and Pointer Operators
Learn how to use the address-of operator & and the dereference operator * in Go, including addressability rules, nil pointer handling, and common pitfalls.
Go provides two operators that work directly with memory: the address operator & and the dereference operator *. They are the foundation of pointer manipulation, allowing you to capture where a value lives in memory and later reach back into that location to read or modify the value. Unlike higher-level abstractions that hide memory details, these operators give you explicit control—but they also require you to understand a few sharp edges.
The Address-Of Operator (&)
The & operator returns the memory address of a variable. If you have a variable x of type T, the expression &x yields a pointer of type *T that points to x. The result is a value you can store, pass to functions, or inspect.
package main
import "fmt"
func main() {
count := 42
ptr := &count
fmt.Printf("count value: %d\n", count)
fmt.Printf("ptr holds: %v\n", ptr)
fmt.Printf("ptr type: %T\n", ptr)
}
Running this prints something similar to:
count value: 42
ptr holds: 0xc000014088
ptr type: *int
The concrete address will differ on your machine, but the key observation is that ptr is not 42—it is a pointer to the location of count. The type *int tells you it points to an integer. That small asterisk in the type is part of the pointer type, not the dereference operator; the context determines which role it plays.
A function that receives a pointer can reach back and alter the original variable. Without &, you would have to copy the value.
func increment(n *int) {
*n = *n + 1
}
func main() {
value := 10
increment(&value)
fmt.Println(value) // 11
}
&value captures the address of value, increment receives it, and *n on the left-hand side of the assignment writes to that location. This is the standard pattern for out-parameter behavior in Go.
& is Not the Same as a Reference:
Many languages use & to denote a reference type. In Go, & always produces a pointer value that you must explicitly dereference with *. There is no automatic dereferencing like in C++ references.
Addressability Rules
Not every expression can have its address taken. The Go specification defines the notion of addressability: an operand is addressable if it is a variable, a pointer indirection, a slice indexing operation, a field selector of an addressable struct, or an array indexing operation of an addressable array. Essentially, you can take the address of something that has a stable, identifiable location in memory.
The rule catches many beginners by surprise when they try to write:
p := &42 // compile error: cannot take address of 42
A literal 42 is a constant with no dedicated memory location at the source level, so &42 is illegal. Similarly, the return value of a function call is not addressable:
func f() int { return 7 }
p := &f() // compile error: cannot take address of f()
Map index expressions are also not addressable because map entries move during resizing:
m := map[string]int{"a": 1}
p := &m["a"] // compile error: cannot take address of m["a"]
The compiler rejects these attempts at compile time. Recognizing what is and isn't addressable prevents a whole category of bugs before they even run.
Addressability is a Compile-Time Gate:
If you attempt to take the address of a non-addressable operand, the program will not compile. This is not a runtime panic—it is a hard error you must fix. Common offenders: literals, function call results, map index expressions, and constant values.
Composite Literal Exception
The specification carves out a deliberate exception: a composite literal may be addressed even though a regular literal cannot. This lets you create a pointer to a new struct, slice, or array directly.
type Config struct {
Port int
Host string
}
cfg := &Config{
Port: 8080,
Host: "localhost",
}
Here &Config{...} is valid. The compiler allocates a Config value and returns its address. This shortcut is used constantly in Go, especially when creating struct pointers with initial values.
numbers := &[]int{10, 20, 30}
The same rule applies: & in front of a composite literal is always legal, so you can directly obtain a pointer to a newly allocated slice or array.
Composite Literals Simplify Allocation:
Using &StructType{...} is idiomatic Go. It replaces a separate variable declaration plus &variable and makes code more concise without losing safety.
Workarounds for Non-Addressable Values
When you genuinely need a pointer to a scalar literal, you have a few options. The most common before generics was to use a slice indexing trick:
p := &[]int{3}[0]
A slice indexing operation is addressable, so this compiles. However, it is not particularly readable. With generics, a cleaner solution emerged: a small helper function.
package ptr
func To[T any](v T) *T {
return &v
}
Now you can write:
port := ptr.To(3000)
The parameter v is a local variable inside To, so it is addressable. The function returns a pointer to it. This pattern has become so popular that many large Go codebases include a ptr package with just this function.
The Dereference Operator (*)
The * operator reads or writes the value at the memory location held by a pointer. Given a pointer p of type *T, the expression *p yields the T value stored there. You can use it on the right-hand side of an assignment to read, or on the left-hand side to write.
x := 5
p := &x
fmt.Println(*p) // prints 5
*p = 10
fmt.Println(x) // prints 10
On the left, *p is an addressable expression that represents the variable x itself. That's why *p = 10 directly modifies x. This is the mechanism behind out-parameters and any function that receives a pointer to mutate the caller's data.
When you see * in a type, like var p *int, the asterisk is part of the type notation, not the dereference operator. The same symbol plays two roles: a type modifier meaning "pointer to" and a unary operator meaning "value at." Context tells them apart. In *p, the star is the operator; in *int, it is the type.
The Same Star, Two Meanings:
Don't confuse the dereference operator with pointer-type syntax. When you see *int, think "pointer to int." When you see *p where p is a pointer, think "value at p." The compiler knows the difference, but reading code correctly takes practice.
Nil Pointers and Panics
Every pointer variable in Go has a zero value of nil. A nil pointer points to nothing. If you attempt to dereference a nil pointer, the runtime throws a panic with the message "runtime error: invalid memory address or nil pointer dereference".
var p *int
fmt.Println(*p) // panic: runtime error: invalid memory address or nil pointer dereference
This is the most common runtime panic in Go programs. It occurs whenever a nil pointer reaches a dereference operation—often through an uninitialized struct field, a missing return check, or a nil interface.
A nil pointer is not the same as a pointer to a zero value. *int(nil) cannot be dereferenced, while new(int) returns a valid pointer to a zero value. This distinction is critical for functions that accept pointer parameters: a nil pointer may mean "no value" rather than "zero."
func safeDeref(p *int) int {
if p == nil {
return 0
}
return *p
}
Checking for nil before dereferencing is the standard defensive pattern. Many Go libraries document whether a nil pointer is acceptable and what behavior to expect.
Nil Pointer Dereference is a Runtime Panic:
Unlike addressability errors, nil dereference is not caught at compile time. It will crash your program. Always initialize pointers before use, and guard against nil in functions that receive pointers from callers.
Common Mistakes and Pitfalls
Confusing the two operators leads to several well-known errors. One is trying to take the address of a function's return value directly:
func getValue() int { return 100 }
ptr := &getValue() // compile error
A value returned by a function is ephemeral—it doesn't occupy a permanent location you can point to. To get a pointer, assign the result to a variable first and then take its address.
Another frequent misstep is forgetting to dereference when passing a pointer to a function that expects a value, or vice versa. The compiler will complain about type mismatches, but beginners often try to force a conversion that does not exist.
Nil pointer dereference in struct fields is a trap when you embed pointers without initialization:
type Person struct {
Name *string
}
func main() {
p := Person{}
fmt.Println(*p.Name) // panic: nil pointer dereference
}
The field Name is a nil pointer because no value was assigned. The correct approach is either to initialize the field with a valid pointer or to check for nil before using it.
A more subtle issue: taking the address of an iteration variable inside a loop. The iteration variable is reused across loop iterations, so all pointers captured inside the loop body point to the same memory location.
var out []*int
for i := 0; i < 3; i++ {
out = append(out, &i)
}
for _, ptr := range out {
fmt.Println(*ptr) // prints 3, 3, 3 not 0,1,2
}
The loop variable i changes each iteration, but the address &i remains constant. By the time you dereference the stored pointers, they all see the final value. The fix is to declare a new variable inside the loop body whose address you take.
for i := 0; i < 3; i++ {
i := i // shadow creates a new variable
out = append(out, &i)
}
Practical Real-World Usage
Pointers in Go appear most frequently in three scenarios:
- Modifying function arguments – When a function needs to change the caller's variable, pass a pointer. This is the idiomatic replacement for reference parameters found in other languages.
- Avoiding large copies – Passing a pointer to a large struct is cheaper than copying the entire struct. However, the Go compiler's escape analysis may allocate the struct on the heap rather than the stack, so the performance trade-off depends on context. Use pointers for large structs when profiling indicates a real benefit.
- Implementing methods with pointer receivers – A method with a pointer receiver can modify the receiver and avoids copying. Even for types that don't need modification, a pointer receiver is common when the type is large or when you want consistency across methods.
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
Without *Counter, Increment would modify a copy, and the original would remain unchanged. The caller typically writes counter.Increment(), and Go automatically takes the address of counter if the variable is addressable.
Nil Receivers Are Possible:
A method with a pointer receiver can be called on a nil pointer. If you dereference the receiver inside the method without checking for nil, it will panic. Defensive nil checks inside methods are a good practice when nil is a valid argument.
The & operator also plays a role when constructing values that implement interfaces via pointer receivers. A type *T often satisfies an interface, while T does not. Consequently, you frequently pass &myStruct when the interface expects a pointer.
var w io.Writer = &myWriter{}
Here myWriter implements Write with a pointer receiver, so the variable must be a *myWriter to satisfy io.Writer.
Summary
The address-of operator & and dereference operator * are the building blocks of pointer manipulation in Go. They give you the ability to share memory locations, avoid copies, and build mutable abstractions—all while keeping control explicit and visible in the code. The addressability rules enforce compile-time safety: you cannot accidentally point to ephemeral values. Composite literals are the pragmatic exception that makes initializing pointers to structs ergonomic.
If you find yourself frequently writing ptr.To() wrappers for scalars, you might want to create a small helper package. When you receive a pointer from an external function, check for nil before you touch it, or document that a nil pointer means something specific. The loop variable capture trap is subtle but entirely avoidable once you know it exists.