Address and Dereference Operators

Learn how the address operator (&) and dereference operator (*) work in Go, with practical examples and common pitfalls.

Pointers hold memory addresses. To get the address of a variable you use the address operator (&). To read or change the value stored at that address you use the dereference operator (*). These two operators are the simplest, most direct way to work with pointers. Everything else — passing pointers to functions, pointer receivers, pointer to struct fields — builds on them.

The Address Operator (&)

Placing & before a variable produces a pointer to that variable. The pointer’s type is *T, where T is the type of the original variable.

package main
import "fmt"
func main() {
    x := 42
    p := &x
    fmt.Printf("Type of p: %T\n", p)   // *int
    fmt.Printf("Value of p: %v\n", p)  // e.g. 0xc000012090
}

p now holds the address of x. That address changes each time you run the program, but the type *int is always the same. The & operator works on any variable — integers, floats, strings, structs, array elements, slice elements — anything that can sit at a known location in memory.

You can also take the address of a composite literal, like &MyStruct{Field: 10}, or of an indexed element &slice[2]. What you cannot do is take the address of a raw literal constant or an expression that has no fixed location, such as &42 or &(x + 1). The compiler will reject those with a message like “cannot take the address of …”.

Only Addressable Operands:

The & operator requires an addressable operand — a variable, a struct field, an array or slice element, or a composite literal. Expressions like &10 or &("hello" + "world") are compile-time errors. If you need a pointer to a constant value, create a variable first.

A common shorthand is to declare and initialise a pointer in one line:

y := 100
ptr := &y

The compiler infers that ptr is *int without you writing the type explicitly.

The Dereference Operator (*)

If you already have a pointer, the * operator gives you access to the value it points to. This is called dereferencing or indirecting.

x := 10
p := &x
fmt.Println(*p) // prints 10

*p reads the value at the address held by p. But you can also write through the pointer: assigning a value to *p updates the original variable.

x := 10
p := &x
*p = 25
fmt.Println(x) // prints 25

Because p holds the address of x, the assignment *p = 25 changes the memory at that address — which is exactly x’s storage. Any other part of the program that reads x will see 25.

Modification Through a Pointer Works:

When you write *p = newValue, the change is visible anywhere the original variable is used. This is the foundation of “pass-by-pointer” patterns in Go.

The * operator can be applied repeatedly for pointers to pointers, though that is rare. Dereferencing a **int once gives a *int, and dereferencing again gives an int. Each * peels off one level of indirection.

How & and * Relate to Each Other

The two operators are inverses in a precise sense:

  • *&x evaluates to the original variable x. You can use it just like x — even on the left side of an assignment: *&x = 30 works and changes x.
  • &*p evaluates to p itself, provided p is a valid, non-nil pointer. It takes the value at p (via *p) and then gets its address again, which is exactly p.

These identities reflect the fact that & and * cancel each other out. You won’t often write them back-to-back in real code, but understanding the relationship solidifies your intuition about what each operator does at the memory level.

A Complete Example

The following program walks through each operation step by step: creating a variable, taking its address, reading through the pointer, modifying through the pointer, and verifying the original changed.

package main
import "fmt"
func main() {
    i, j := 42, 2701
    p := &i                // p points to i
    fmt.Println(*p)        // read i through the pointer: 42
    *p = 21                // set i through the pointer
    fmt.Println(i)         // i is now 21
    p = &j                 // point p to j instead
    *p = *p / 37           // read j, divide, assign back
    fmt.Println(j)         // j is now 73 (2701 / 37)
}

When p is reassigned to &j, it no longer points to i. The original connection to i is lost — but i still exists with its last value. Changing *p after p = &j affects j, not i. The pointer is just a variable that can be updated to point somewhere else.

Pointers Are Reassignable:

A pointer variable is a value of its own. You can change where it points at any time. The underlying variables it points to remain independent.

The Most Common Mistake: Dereferencing a Nil Pointer

A pointer declared without initialisation has the value nil. Dereferencing a nil pointer triggers a runtime panic — a crash — with a message like “invalid memory address or nil pointer dereference”.

var p *int      // p is nil
fmt.Println(*p) // panics

Nil Pointer Dereference Crashes the Program:

Any dereference of a nil pointer causes an immediate runtime panic. Always ensure a pointer is non-nil before you use * on it. If you receive a pointer from a function, check for nil unless the function guarantees a valid return.

You can check for nil:

if p != nil {
    fmt.Println(*p)
}

This is especially important when working with pointers returned by functions that may fail, or when dealing with optional fields in structs.

Distinguishing * for Types and for Dereferencing

The same * symbol appears in two places with different meanings:

  • In a type declaration, * means “pointer to”. Example: var p *int declares p as a variable of type “pointer to int”.
  • In an expression, * is the dereference operator. Example: *p reads the value at the address stored in p.

The compiler never confuses them because context determines which one applies. When you see * in front of a type, it’s describing a type. When you see * in front of a value, it’s an operation.

Many beginners try to read var p *int = &x as “dereference p is int …”, but that mental model breaks. Think of it as: “p is a variable whose type is *int”, and *int means “pointer to int”.

Pointers and the “Copy” Behaviour

A common point of confusion is whether *p gives you the original value or a copy. The answer depends on what you do next.

  • Reading *p produces a copy of the value at that address, just like reading x directly.
  • Writing to *p writes through the pointer to the original location — so the original variable changes.

This is exactly the same as working with the variable directly. x gives you a copy of the value; x = 5 changes the original. The pointer just provides an indirect path to the same memory.

A subtle implication: if you assign the result of a dereference to a new variable, that new variable gets a copy.

x := 10
p := &x
copy := *p    // copy is now 10
*p = 20       // changes x, but copy remains 10

This is not a pointer-specific oddity; it’s just Go’s copy-by-value assignment. Once the value is copied into copy, changes to the source don’t propagate backward.

Real-World Use: Modifying Values Inside a Function

The primary reason you’ll use & and * in practice is to let a function modify a caller’s variable. Without pointers, a function receives a copy and the caller’s variable stays untouched.

func increment(val *int) {
    *val = *val + 1
}
func main() {
    count := 0
    increment(&count)
    fmt.Println(count) // 1
}

The caller passes the address with &count. Inside the function, *val refers to the original count variable, so the increment is visible after the function returns. This pattern shows up constantly in Go standard library functions like fmt.Scan or json.Unmarshal, which need to write results into variables the caller owns.

Summary

The address operator & and the dereference operator * are the essential building blocks of pointer manipulation in Go. & turns a variable into a pointer; * turns a pointer back into the variable it points to. Together they allow you to share data without copying it and to modify values across function boundaries.

A handful of rules keep them safe: you can only take the address of something that has a fixed location, and you can only dereference a non-nil pointer. If you remember those two constraints, the rest is just practice.