Pointer Fundamentals
Learn what pointers are in Go, how to declare and use them, the & and * operators, and how to work with pointers to structs
Every variable you create in a Go program lives somewhere in memory. A pointer is a variable that stores that somewhere — the memory address of another variable. Instead of holding a name, a number, or a string directly, it holds a location you can follow to find the actual data.
This concept matters because not all data is small and cheap to copy around. When you pass a large struct to a function without a pointer, Go duplicates the entire thing. If the function needs to change the original, that duplicate is useless. Pointers solve both problems: they let you share data without copying it and let you modify it in place.
The three building blocks that make pointers useful — and sometimes confusing — are the pointer type itself, the operators that work with memory addresses, and the way pointers interact with Go's struct types.
Pointer Types and Declaration
A pointer type is written by placing a * in front of the type it points to. A *int points to an integer. A *string points to a string. A *MyStruct points to a value of type MyStruct. The pointer itself is just a variable that can hold a memory address.
The zero value of any pointer type is nil. A nil pointer doesn't point to any valid memory — it is essentially an empty container waiting for an address.
Nil Pointer Panic:
Attempting to read or write through a nil pointer — that is, dereferencing it — causes a runtime panic. Always ensure a pointer is non-nil before using it, or initialize it with a valid address at declaration time.
Declaring a pointer explicitly looks like any other variable declaration, with the * before the type:
var p *int
At this point p is nil. It exists, but it doesn't point to anything yet. To make it useful, you need to store the address of an existing int variable inside it.
You can declare and initialize a pointer in one line using the address operator &:
x := 42
p := &x
Here the compiler infers the type of p as *int from the assignment. This short variable declaration form is the most common way to create pointers in everyday Go code.
var p *int = &x
This longer form explicitly states both the pointer type and the assignment. It does exactly the same thing, but the explicit type can make the intent clearer in some contexts.
All three approaches produce a pointer to x. The value stored inside p is the hexadecimal memory address of x — something like 0xc0000140a0. You can print it with fmt.Println(p).
Every pointer in Go is strongly typed. A *int cannot hold the address of a string, and the compiler will reject any attempt to mix types. This is a guardrail that prevents entire categories of low-level mistakes without requiring the programmer to think about memory layout.
Address and Dereference Operators
The & operator generates the memory address of a variable. When you write &x, you get a pointer to x — not the value x contains, but the location where that value lives. The * operator does the reverse: given a pointer, it follows the address and gives you back the actual value sitting there. This reverse action is called dereferencing.
The following example shows both operators working together. It creates an integer, takes its address, reads it through the pointer, and then writes a new value through that same pointer.
package main
import "fmt"
func main() {
count := 10
ptr := &count
fmt.Println("Value through pointer:", *ptr) // reads count via ptr
*ptr = 25 // writes to count via ptr
fmt.Println("New value of count:", count) // prints 25
}
First, ptr := &count stores the address of count. When *ptr appears on the right side of a Println, it dereferences the pointer — "go to the address stored in ptr and grab whatever is there" — which yields 10. Then *ptr = 25 appears on the left side of an assignment. This tells Go to go to that same address and overwrite the contents with 25. After that line, count itself is 25 because the pointer pointed directly at its memory.
The * operator serves two distinct roles in Go syntax, and this trips up many newcomers. In a type declaration like var p *int, the * means "this variable is a pointer to int." In an expression like *p = 42, the * means "follow the pointer and access the value." The same symbol, different meanings based on where it appears.
Declaration vs. Dereference:
The * before a type (*int) defines a pointer type. The * before a pointer variable (*p) dereferences it to access the underlying value. Treating one as the other leads to compile errors or logic bugs.
The & operator can only be applied to addressable values. You can take the address of a variable, an element of a slice or array, or a field of a struct. You cannot take the address of a constant, a literal value like &10, or a temporary expression result. Go enforces that the thing whose address you're taking actually has a stable place in memory.
Go does not have pointer arithmetic. You cannot add or subtract integers from a pointer to walk through memory. This means no off-by-one overruns into adjacent data and no buffer overflow bugs caused by pointer math. If you need to traverse contiguous memory, Go gives you slices, which do that safely and idiomatically.
No Pointer Arithmetic:
Unlike C, Go disallows pointer arithmetic. You can't increment or decrement a pointer, nor can you perform numeric operations on it. This eliminates a major source of memory safety bugs.
Pointers to Structs
Structs often hold more than a few bytes of data, and copying them repeatedly can become expensive. Pointers to structs let functions and methods operate on the original data without duplication. They also allow the struct's fields to be modified by any code that holds the pointer.
You create a pointer to a struct the same way you create any pointer — by taking the address of a struct value.
type Person struct {
Name string
Age int
}
alice := Person{Name: "Alice", Age: 30}
p := &alice
p is now a *Person that points to alice. You can access the fields of the struct through the pointer without explicitly dereferencing it. Go automatically interprets p.Name as (*p).Name, so the syntax stays clean.
fmt.Println(p.Name) // prints "Alice"
p.Age = 31 // modifies the original struct
fmt.Println(alice.Age) // prints 31
The second line writes 31 into the Age field of the struct that p points to — which is alice itself. After that line, alice.Age reflects the change.
Field Access is Seamless:
You don't need to write (*p).Field in Go. The compiler handles the dereference automatically when you access struct fields through a pointer, keeping the code readable.
An especially convenient shorthand creates and points to a struct in one step:
bob := &Person{Name: "Bob", Age: 25}
This allocates a new Person, initializes its fields, and returns a *Person pointing to it. It is the most common way to obtain a struct pointer in Go, particularly when building data that will be shared or mutated across function boundaries.
When a struct pointer is nil, any attempt to access its fields will panic. This often happens when a variable is declared but never assigned a real struct.
var ptr *Person
fmt.Println(ptr.Name) // panic: runtime error: invalid memory address or nil pointer dereference
Always initialize a struct pointer — either by taking the address of an existing struct or by using the &Person{} composite literal — before reaching for its fields.