How to Dereference a Pointer in Go

To dereference a pointer in Go, use the asterisk (`*`) operator on the pointer variable to access the underlying value it points to.

How to Dereference a Pointer in Go

You're building a cache. You have a CacheEntry struct that holds a timestamp, a key, and a payload. You pass this struct to a function that updates the timestamp. If you pass the struct by value, the function gets a copy. The original timestamp stays stale. You need to point to the original data so the change sticks. That's where dereferencing comes in. You hold the address, and the asterisk operator lets you reach through that address to touch the value itself.

Pointers are labels, dereferencing is opening the box

A pointer is a label stuck on a box. The label has the address of the box. The value is inside the box. Dereferencing is peeling back the label to look inside the box. The * operator is your hand reaching through the label. If you have a pointer p, *p is the value at the address p holds. It's not a copy. It's the thing itself.

The & operator creates the label. It takes a value and returns its address. The * operator removes the label. It takes a pointer and returns the value. They are inverses. *&x is the same as x. &*p is the same as p, provided p is not nil.

The zero value of a pointer is nil. If you declare var p *int, p is nil. It points nowhere. Dereferencing nil crashes the program. This is why nil checks matter. A nil pointer is a valid pointer value, but it carries no address.

Minimal example

Here's the bare mechanics: create a pointer, dereference to read, dereference to write.

package main

import "fmt"

func main() {
    // value lives on the stack
    count := 10
    // ptr holds the address of count
    ptr := &count

    // *ptr accesses the value at that address
    fmt.Println("Before:", *ptr)

    // modify the value through the pointer
    *ptr = 42
    // count changed because ptr points to the same memory
    fmt.Println("After:", count)
}

The asterisk is a bridge, not a copy.

What happens at runtime

When you write ptr := &count, the compiler allocates space for count and space for ptr. ptr stores the memory address of count. When you write *ptr, the runtime looks at the address stored in ptr, goes to that location in memory, and fetches the bits there. Writing *ptr = 42 does the reverse: it takes the address, goes there, and overwrites the bits. The variable count and the expression *ptr refer to the exact same memory location. Changing one changes the other.

Dereferencing is an expression. *ptr evaluates to the value. You can use it anywhere a value is expected. x := *ptr assigns the value. fmt.Println(*ptr) prints the value. *ptr++ increments the value. The * binds tightly. *ptr + 1 adds one to the value, it does not move the pointer. This is a common confusion for C programmers. Go makes the distinction clear: pointers and values are different types. You cannot add an integer to a pointer. You add an integer to the dereferenced value.

Go pointers are different from C pointers. You cannot add or subtract from a pointer. There is no ptr++ to walk through an array. The compiler rejects pointer arithmetic with invalid operation: ptr + 1 (mismatched types *int and untyped int). This restriction keeps memory safe. You use slices and indices to traverse data, not raw pointer math. Slices handle the underlying array access with bounds checking. Pointer arithmetic is a frequent source of buffer overflows in other languages. Go removes the capability to eliminate the bug class entirely.

Trust the type system. Pointers point. Values hold.

Escape analysis: the compiler moves data for you

You might take the address of a local variable and return it. In languages like C, this is undefined behavior because the local variable is destroyed when the function returns. Go handles this automatically. The compiler performs escape analysis. If a pointer to a local variable escapes the function, the compiler moves that variable to the heap. You don't write malloc. You don't manage lifetimes. & is safe regardless of where the data lives.

func makeCounter() *int {
    // count is local, but returned, so it escapes to the heap
    count := 0
    return &count
}

func main() {
    // c points to heap memory managed by the garbage collector
    c := makeCounter()
    *c++
    fmt.Println(*c)
}

The runtime tracks the heap allocation and frees it when no pointers reference it. You get the safety of automatic memory management with the flexibility of pointers.

Don't worry about stack or heap. Write the code. The compiler optimizes the layout.

Realistic example

Pointers shine when you need to mutate state across function boundaries without returning the whole struct. Here's a realistic pattern: a function that updates a user's balance in place.

package main

import "fmt"

type User struct {
    Name    string
    Balance int
}

// AddFunds updates the balance of the user pointed to by u
func AddFunds(u *User, amount int) {
    // dereference u to access the struct fields
    u.Balance += amount
}

func main() {
    // create a user value
    alice := User{Name: "Alice", Balance: 100}

    // pass the address of alice to AddFunds
    AddFunds(&alice, 50)

    // alice.Balance is now 150
    fmt.Println(alice.Balance)
}

Notice the receiver name u. Go convention prefers short names that match the type, like u for User or b for Buffer. Avoid self or this. Those come from other languages and clutter Go code. The community expects one or two letters.

Methods can have pointer receivers. A method with func (u *User) UpdateBalance can change the user. A method with func (u User) GetBalance cannot. The receiver type determines mutability. If the method needs to change the struct, the receiver must be a pointer.

Pointer receivers mutate. Value receivers observe.

Pointer comparison vs value comparison

Comparing pointers checks identity. Comparing dereferenced values checks equality. Two pointers can point to different memory locations holding the same value. p1 == p2 is false. *p1 == *p2 is true. This distinction matters when you need to know if two references point to the same object versus if they hold equivalent data.

func main() {
    a := 10
    b := 10
    pa := &a
    pb := &b

    // pa and pb point to different variables
    fmt.Println(pa == pb) // false

    // the values they point to are equal
    fmt.Println(*pa == *pb) // true
}

Use pointer comparison when checking identity. Use value comparison when checking content.

Pointer to pointer: usually a code smell

You can have a pointer to a pointer. **int is a pointer to an *int. Dereferencing once gives you the inner pointer. Dereferencing twice gives you the integer. This pattern allows a function to modify the pointer itself, not just the value it points to.

func initPtr(pp **int) {
    // allocate a new int and store its address in *pp
    *pp = new(int)
    // set the value of the int
    **pp = 42
}

func main() {
    var p *int
    initPtr(&p)
    fmt.Println(*p) // 42
}

This is rare in Go. Most code that reaches for **T can be rewritten to return the pointer instead. Returning is clearer and avoids double indirection.

func makePtr() *int {
    p := new(int)
    *p = 42
    return p
}

Prefer returning pointers over pointer-to-pointer parameters. The call site reads left to right. p := makePtr() is easier to follow than initPtr(&p).

Keep indirection shallow. Return the pointer.

Pitfalls and compiler errors

The most common crash in Go is dereferencing a nil pointer. A nil pointer holds no address. If you try to read *nil, the runtime panics with invalid memory address or nil pointer dereference. This stops the program immediately. Always check for nil before dereferencing if the pointer might be nil.

func safePrint(p *int) {
    // check nil to avoid panic
    if p == nil {
        fmt.Println("no value")
        return
    }
    // safe to dereference now
    fmt.Println(*p)
}

Nil checks are cheap. Panics are expensive.

You can't take the address of everything. Map values are not addressable. If you try addr := &m["key"], the compiler says cannot take the address of map index expression. Map values can move in memory as the map grows. Taking an address would create a dangling pointer. Go prevents this at compile time. If you need to update a map value, read it, modify a local copy, and write it back.

You rarely need a pointer to a string. Strings are immutable and cheap to pass by value. Passing *string adds indirection without saving memory. The compiler won't stop you, but the code becomes harder to read. Pass the string directly unless you have a specific reason to allow the caller to reassign the string variable itself.

Don't fight the type system. Wrap the value or change the design.

When to use pointers

Use a pointer when you need to modify the original value inside a function. Use a pointer when passing large structs to avoid copying the data. Use a value when the data is small and immutable, like an integer or a short string. Use a pointer when you need to represent the absence of a value with nil. Use a value when you want to guarantee the caller cannot mutate the data.

Pointers are for mutation and sharing. Values are for safety and simplicity.

Where to go next