What Do * and & Mean in Go

In Go, * dereferences a pointer to access its value, while & takes the memory address of a variable.

The missing link in your function

You write a function to update a user profile. You pass the struct, change a field inside the function, and return. When you print the original struct back in main, the field never changed. You stare at the code. The logic is correct. The syntax is valid. The value just refuses to update.

The fix is two symbols you have probably seen but never fully understood: & and *. They are not mathematical operators. They are memory operators. One gives you a location. The other opens the door at that location. Master them and Go stops feeling like a language that copies everything behind your back.

The mental model: addresses and doors

Every variable in Go lives somewhere in memory. The runtime assigns it a numeric address. You rarely see those numbers, but they exist. Think of memory like a hotel with millions of rooms. Each room has a number on the outside. Inside the room is the actual data.

The & symbol is the concierge. You hand it a variable name and it hands back the room number. It does not give you the contents. It gives you the address.

The * symbol is the key card. You hand it a room number and it opens the door so you can read or change what is inside. It works on addresses, not on variable names.

Go pointers are deliberately simple. You cannot add or subtract numbers from them. You cannot chain them together like in C. The garbage collector tracks them automatically. If nothing points to a room anymore, the runtime clears it out. You get reference semantics without the memory management headaches.

Pointers are just addresses. Treat them like addresses and the rest follows.

A minimal example

Here is the simplest pointer interaction: create a value, take its address, and modify it through the pointer.

package main

import "fmt"

func main() {
    // start with a plain integer on the stack
    x := 10

    // &x asks the compiler for the memory address of x
    // p is now a pointer to an int, holding that address
    p := &x

    // *p follows the address and reads the value stored there
    fmt.Println(*p) // prints 10

    // *p follows the address again, but this time writes a new value
    // the change happens at the original memory location
    *p = 20

    // x and *p point to the same room, so x sees the update
    fmt.Println(x) // prints 20
}

The first line creates x. The second line creates p. p does not hold 10. It holds something like 0xc000010238. When you write *p, the runtime looks up that address and fetches the integer. When you assign to *p, the runtime writes to that exact address. x and *p are two different names for the same storage.

Pointers are just addresses. Treat them like addresses and the rest follows.

What the compiler and runtime actually do

When you compile the program, the Go compiler decides where each variable lives. Small, short-lived variables usually sit on the stack. The stack is fast and automatically cleaned up when a function returns. Larger or longer-lived variables may be placed on the heap. The heap survives past function boundaries and is managed by the garbage collector.

You do not control this placement directly. The compiler runs escape analysis. If a variable might outlive the function that created it, it escapes to the heap. If it stays local, it stays on the stack. Pointers work identically in both cases. The address is just a number. The runtime knows how to read and write it regardless of where it points.

Go also guarantees pointer safety at compile time. You cannot take the address of a literal constant. You cannot perform pointer arithmetic. You cannot cast a pointer to an integer and back without using the unsafe package, which requires an explicit import and is heavily discouraged. The language forces you to work with typed pointers. *int is a different type from *string. The compiler rejects mixing them.

The compiler decides placement. You just follow the address.

Pointers in real code

Pointers shine when you need to modify data without copying it, or when you need to share a single piece of state across multiple functions. The most common place you will see them is in method receivers.

Here is a realistic pattern: a struct that tracks a counter, with a method that increments it.

package main

import "fmt"

// Counter holds a running total
type Counter struct {
    total int
}

// Increment adds one to the counter
// pointer receiver allows the method to modify the original struct
func (c *Counter) Increment() {
    c.total++
}

func main() {
    // create a struct value
    c := Counter{total: 0}

    // call the method on the value
    // Go automatically takes the address because the receiver is a pointer
    c.Increment()

    // the original struct was modified in place
    fmt.Println(c.total) // prints 1
}

Notice the receiver syntax: (c *Counter). The convention is to use a one or two letter name that matches the type. c for Counter, u for User, s for Server. Do not use this or self. Those belong to other languages and break Go tooling expectations.

When you call c.Increment(), c is a value, not a pointer. Go is smart enough to automatically take the address behind the scenes. It translates the call to (&c).Increment(). You get pointer semantics without typing the & manually. This automatic addressing only works when the receiver is a pointer type. If the receiver were (c Counter), Go would pass a copy, and the increment would vanish.

Method receivers follow the type. Match the receiver to the mutation pattern.

Where things go wrong

Pointers introduce a small set of predictable failure modes. The compiler catches most of them. The runtime catches the rest.

The most common crash is dereferencing a nil pointer. A pointer variable starts as nil until you assign it an address. If you try to read or write through a nil pointer, the program stops immediately. The runtime panics with invalid memory address or nil pointer dereference. This is Go's way of telling you that you are trying to open a door that does not exist. Always initialize pointers before dereferencing them, or check for nil explicitly if the pointer might be empty.

Another trap is taking the address of a loop variable in older Go versions. Before Go 1.22, loop variables were reused across iterations. If you captured &i inside a loop, every pointer would point to the same memory slot, holding the final value. The compiler now rejects this pattern with loop variable i captured by func literal. The fix is to declare a new variable inside the loop body, or upgrade to Go 1.22+ where the compiler handles it correctly.

You will also see compiler rejections when you try to take the address of something that does not have a stable location. Expressions like &(a + b) or &func() fail because the result is a temporary value that disappears at the end of the statement. The compiler complains with cannot take the address of temporary value. Assign the result to a named variable first, then take its address.

Nil pointers crash. Temporary values cannot be addressed. Trust the compiler errors.

When to reach for pointers

Go defaults to passing values. Copies are cheap for small structs, strings, and slices. Pointers are an explicit choice. Use them when the semantics require shared state or when copying would waste time and memory.

Use a value when the data is small and you only need to read it. Use a pointer when you need to modify the original data inside a function or method. Use a pointer when the struct is large and copying it would trigger unnecessary memory allocation. Use a value when you want to guarantee that a function cannot mutate the caller's state. Use a pointer when you need to represent an optional or missing value with nil. Use a slice or map when you need a dynamically sized collection that is already reference-backed. Use plain sequential code when you don't need shared state: the simplest thing that works is usually the right thing.

Go copies by default. Opt into pointers only when the design demands it.

Where to go next