AlgoMaster Logo

Pointer Basics

Last Updated: May 22, 2026

High Priority
13 min read

A pointer is a variable that holds the memory address of another variable instead of holding the value directly. Once you have that address, you can read or change the original value from anywhere that has the pointer, without copying it. This lesson covers what a pointer is, the & and * operators, the *T type syntax, how to declare pointer variables, the nil zero value, and how to compare pointers.

Why Pointers Exist

Every variable in a Go program lives somewhere in memory. When you write price := 24.99, Go picks an address in memory, stores the bytes for 24.99 there, and remembers that the name price refers to that address. Most of the time you never think about the address. You read and write the variable by name and Go handles the rest.

Pointers let you talk about the address itself. Instead of carrying around a copy of a value, you can carry the address of the original and reach back to it whenever you need.

To see why that's useful, here's a small program that tries to update a product's stock count from a helper function:

The stock didn't move. sellOne received its own copy of the Product struct, decremented the copy's Stock field, and threw the copy away when it returned. The caller's book was never touched.

There are two ways out of this. You can return the modified struct and have the caller reassign it, or you can give the function a pointer to the original so it can change the value in place. The second option is what pointers are for, and it shows up all over Go: in methods that mutate their receiver, in functions that fill in a result, in slices and maps that share backing data, in concurrency primitives like sync.Mutex. Before any of that makes sense, you need a clear picture of what a pointer is and how to use one on a single variable.

Memory, Addresses, and the & Operator

Every variable has an address. The address-of operator, written &, gives you that address as a value you can store, pass around, or print.

The exact hexadecimal address will differ on every run. Go can put the variable wherever it wants, and the runtime may even move it later. What matters is that &price evaluates to a concrete location, and addr now holds that location.

addr is a pointer. Its value is the address 0xc0000140a0, and its type is *float64, which reads as "pointer to a float64". The * in the type name is part of the type, not an operator at this point. We'll meet * as an operator in the next section.

You can take the address of any addressable value: a variable declared with var or :=, a struct field accessed through a variable, or an element of an array or slice. You can't take the address of a literal or the result of an expression that isn't stored anywhere. &24.99 is a compile error, and so is &(a + b).

The three addresses sit close together because the three things (the struct and two of its fields) live in the same region of memory. pBook points at the start of the struct, pPrice points at the Price field inside it, and pStock points at the Stock field. They're three different pointers, each with its own type: *Product, *float64, and *int.

The %p verb in Printf formats a pointer as a hex address. It's the standard way to print where something lives in memory. You'll mostly use it for debugging, not for production output.

The diagram shows the variable price sitting at some address, and the pointer variable addr storing that same address as its value. The pointer doesn't contain the number 24.99. It contains the location where 24.99 lives. The arrow is the conceptual "points to" relationship, not a real piece of memory.

Pointer Types: *T

The type of a pointer is written *T, where T is the type of the value being pointed at. The asterisk is part of the type name. *int is "pointer to int", *float64 is "pointer to float64", *Product is "pointer to Product", and so on.

Each pointer carries the address and the type of what it points at. The type matters because Go uses it to know how many bytes to read when you dereference the pointer, and which fields and methods are available on the value. A *int and a *float64 are different types; you can't assign one to the other, even if they happen to hold the same numeric address.

The pointer type and the value type are paired. &x where x has type T always has type *T. There's no way to take the address of a Product and get back a *float64. Go's type system tracks the relationship strictly.

You'll often see the short form with :=, which lets the compiler infer the pointer type from the right-hand side:

%T prints the dynamic type of a value. The output includes the package name (main) because Product is declared in the main package; for types from other packages you'd see *time.Time, *bytes.Buffer, and so on.

Pointer types compose. A pointer to a pointer is written **T, a pointer to a slice of pointers is *[]*T, and so on. Most of the time you'll be working with simple *T pointers; the deeper combinations show up in specific cases.

The * Dereference Operator

The address-of operator gets you a pointer. The dereference operator, also written * but used in front of a pointer expression, gets you back to the value the pointer points at.

pPrice is the address. *pPrice follows that address and reads whatever value lives there. The expression *pPrice has type float64, the same type as price itself.

Dereferencing isn't only for reading. You can also assign through the dereference, and the write lands on the original variable:

The line *pPrice = 19.99 doesn't change pPrice. The pointer still holds the same address. What changes is the value at that address, which is exactly the variable price. After the assignment, reading price directly and reading through *pPrice give the same answer because they're two paths to the same memory.

This is the whole reason pointers matter. A pointer is a remote control for another variable. Read through it to inspect the variable. Write through it to change the variable. The variable doesn't need to know it's being controlled remotely.

The same operator works on pointers to structs. To read a field through a pointer, you can either dereference and then dot, or rely on a shortcut Go provides:

Both (*pBook).Code and pBook.Code mean the same thing when pBook is a *Product. The short form is what you'll see in idiomatic Go almost every time. The long form is occasionally needed when precedence matters, but in everyday code you can read pBook.Code as "the Code field of whatever pBook points to."

The same shortcut applies when writing. pBook.Stock = pBook.Stock - 1 reads the current stock through the pointer, subtracts one, and writes the new value back through the pointer, all of it landing on the caller's book because that's what pBook points at.

Declaring a Pointer Variable

There are a few ways to declare a pointer variable in Go, and they're all useful in different situations. The first is the explicit var form with a pointer type:

var p *int declares p as a pointer to an int and gives it the zero value for pointer types, which is nil. We'll come back to nil in detail in a moment. The point here is that the declaration creates the pointer variable without yet making it point anywhere.

To make the pointer point at something, you assign it the address of an existing variable:

After the assignment, p holds the address of stock, and dereferencing p gives back the value of stock. The two-step form (declare, then assign) is most common when the pointer's target isn't known at the point of declaration.

When you do know the target, you can declare and initialize in one line. The short form uses := and the type is inferred:

p := &stock is by far the most common way to create a pointer in Go. The right-hand side &stock has type *int, so p is inferred as *int. There's no separate declaration line and no manual type annotation.

You can also declare a pointer that already points at a struct in the same line:

book is a Product value, and pBook is a *Product pointing at it. The mutation through pBook lands on book because both names refer to the same memory.

Two more shapes appear regularly. The first uses the built-in new function to allocate a fresh, zero-valued value and return a pointer to it. For now it's enough to recognize the syntax when you see it. The second is the address-of-composite-literal form, where you create a struct value and take its address in a single expression:

&Product{...} creates a Product value and immediately takes its address, giving you a *Product in one expression. There's no separately named variable to hold the value; the value lives in memory because the program is still holding a pointer to it. Go's escape analysis figures out where to allocate it, and the garbage collector cleans it up once nothing references it.

The Zero Value: nil

The zero value of every pointer type is nil. A pointer variable that's been declared but never assigned an address holds nil, and dereferencing a nil pointer is a runtime error.

All three pointers start as nil because nothing has assigned them an address yet. Go represents nil pointers internally as the address 0, which by convention no real variable lives at. Whether you write *int, *float64, or *Product, the zero value is nil.

nil is a typed value despite how it prints. A nil *int is not the same value as a nil *float64, even though both show up as <nil>. You can compare a pointer to nil directly, and Go uses the pointer's type to know which nil is meant:

Before the assignment, p is nil. After p = &stock, p holds a real address and is no longer nil. The comparison p == nil is how you check whether a pointer is currently pointing at something.

Dereferencing a nil pointer crashes the program at runtime with a panic:

The same crash happens if you read a field through a nil struct pointer (pBook.Code when pBook is nil) or write through one. For now, the lesson is that nil means "no target", and you can't read or write through a pointer that has no target.

A pointer can move between nil and non-nil over its lifetime. Assigning nil back to a pointer is fine and is sometimes used to mark "no longer pointing at anything":

Setting p to nil doesn't change stock. It only changes what p points at, which is now nothing. The variable stock is still there, untouched, just no longer reachable through p.

Pointer typeZero valueResult of dereferencing the zero value
*intnilruntime panic
*float64nilruntime panic
*stringnilruntime panic
*Productnilruntime panic
*[]intnilruntime panic

The pattern is the same for every pointer type. The zero value is nil, and dereferencing it panics.

Reading and Writing Through a Pointer

Now that we've seen the parts in isolation, here's the whole loop: create a value, take a pointer to it, read through the pointer, write through the pointer, and watch the original value change.

book and *pBook are two names for the same memory. Reading through either gives the same answer. Writing through either updates the single underlying value, and the other name sees the change immediately. There's no syncing step. They're not two copies that have to be kept in agreement; they're one storage location with two paths to it.

This is the building block for a lot of Go code. A pointer parameter on a function gives the function direct access to the caller's variable. A pointer receiver on a method gives the method direct access to the struct it's called on. A pointer field inside a struct lets two struct values share the same underlying piece of data. The mechanics are the same in every case: pointers let you read and write a value without copying it.

The diagram shows the Product value book with its three fields, and the pointer pBook referring to it. When you write pBook.Stock = 11, Go follows the arrow from pBook to book, then writes into the Stock field. Reading book.Stock goes straight to the same field. There's only one Stock cell in memory, no matter which name you use to reach it.

Modifying a value through a pointer also works for non-struct types. The pattern is the same; the dereference syntax just shows up more explicitly because there's no field to use the auto-dereference shortcut on:

For a plain int, you have to write *pStock explicitly to get at or change the value. For a struct, pBook.Stock handles the dereference for you. The underlying machinery is identical: follow the address, read or write the value at that location.

Comparing Pointers

You can compare pointers with == and !=. Two pointers compare equal when they point at the same memory, that is, when they hold the same address. Two pointers compare unequal when they point at different memory, even if the values stored at those addresses are equal.

pA and pB both point at variables that hold the value 10, but they point at different variables, so the pointers themselves are not equal. pA and pA2 are two pointers to the same variable, so they're equal. The last comparison dereferences both pointers and compares the values, which are both 10, so it's true.

Pointer comparison is identity comparison, not value comparison. It answers "are these two names for the same memory?", not "do they refer to equal data?". The two questions are different, and conflating them is a common source of bugs.

You can also compare a pointer to nil, which is the most frequent comparison in real code:

describe checks for nil before reading through the pointer. Without that check, the second-to-last line would panic when p is nil and the code tries to read p.Code. Defensive nil checks like this are standard whenever a pointer can be missing.

Pointers of different types can't be compared, even if they happen to hold the same numeric address. *int == *float64 is a compile error. Go's type system treats *int and *float64 as unrelated types, and the only way to compare across pointer types is to convert through unsafe.Pointer, which we won't cover here and which is almost never appropriate for application code.

Here's a table summarizing the comparison rules:

ComparisonResult
p == q where both point at the same variabletrue
p == q where each points at a different variable, even with equal valuesfalse
p == nil where p has never been assigned a targettrue
p == nil after p = &xfalse
p == q where p and q have different pointer typescompile error
*p == *q (dereferenced values)depends on the values, not the addresses

Pointers to Struct Fields and Slice Elements

You can take the address of a struct's field or a slice's element, and the resulting pointer behaves like any other pointer. Reads see the current value at that location, and writes update it in place.

&book.Stock is the address of the Stock field inside book. Writing through pStock modifies that field directly; the surrounding struct is left alone. This is occasionally useful when a function or block of code needs to mutate just one field repeatedly without restating the path each time.

The same works for slice elements. Indexing a slice gives back something that has an address, so &s[i] is a valid pointer:

pMid points at stock[1]. Writing *pMid = 100 updates the second element of the slice's backing array. The slice itself is unchanged in shape (same length, same capacity); only the value at index 1 has been replaced.

Map values are different. You cannot take the address of a map element, because the map's internal layout can shift when entries are added or removed, and a stable address can't be promised. &prices["BOOK-01"] is a compile error, with the message "cannot take the address of prices[\"BOOK-01\"]". If you need to mutate a value that lives in a map, the usual pattern is to read it out, modify it, and write it back. Or, if the value is itself a pointer (map[string]*Product), the map holds a pointer that you can dereference and mutate freely.

The map maps a code to a *Product, and the pointer in each entry refers to a Product value elsewhere in memory. Reading catalog["BOOK-01"] gives back the pointer, and then .Stock follows the pointer and reads (or writes) the field. The mutation works because we're not trying to take the address of the map's slot; we're using the pointer the map already stores.