AlgoMaster Logo

Pointer to Pointer

Last Updated: May 22, 2026

Low Priority
8 min read

A pointer to a pointer is exactly what it sounds like: a value whose address you take to get another pointer. The type is written **T, and it appears only when a function needs to reassign the caller's pointer variable itself, not just mutate what that pointer points at. **T is rare in idiomatic Go, and most of the time a cleaner design avoids it entirely.

What **T Actually Means

A pointer *T holds the address of a T value. A pointer to a pointer, written **T, holds the address of a *T value. The extra star is one more level of indirection: instead of pointing at the data, you point at the variable that points at the data.

Three names, three levels. price is the value. p is a pointer to that value. pp is a pointer to p. To read the underlying number through pp, you dereference twice: once to get p, and again to get the float64.

The mechanics are the same as a single pointer, just stacked. & adds a level of indirection. * removes one. So &price produces a *float64, &(&price) is not legal Go directly (you can't take the address of a temporary), but &p where p is already a *float64 variable gives you a **float64.

The diagram shows the chain: pp points at the variable p, which points at the variable price, which holds the actual float64 value. Each arrow is one level of indirection. Dereferencing walks an arrow backwards: *pp jumps from pp to p, and **pp jumps two arrows back to 29.99.

Declaring and Initializing a **T

You can declare a **T variable with var just like any other type. The zero value is nil, the same as for a single pointer. Without something pointing at, dereferencing it panics, so you need to assign before reading through it.

A more typical setup builds the chain from the ground up. Start with a value, take its address to get a *T, then take the address of that pointer variable to get a **T.

All three reads return the same number because all three names reach the same int in memory, just through different routes.

You can also write through **pp, and the change shows up everywhere because the underlying memory is the same:

Writing through **pp is identical to writing to stock directly. The double dereference resolves to the address of stock, and the assignment stores 99 there. Any name that reaches that address (stock, *p, **pp) sees the new value.

There's another operation that **T is designed for: writing through *pp (single dereference) replaces the *T pointer itself.

That's the key operation. *pp = &mugPrice doesn't change a float64. It changes the pointer variable current to point at a different float64. After the assignment, current no longer points at bookPrice, it points at mugPrice. That's something a plain *float64 parameter to a function can't do, and it's the one job **T is actually good at.

Why You'd Ever Need This

Passing a *T to a function lets the function mutate the pointed-at value. What it doesn't let the function do is change which thing the caller's pointer variable points at. Those are two different operations.

discount worked. swapToBook did nothing visible to the caller. The reason is value semantics: when current is passed to swapToBook, the function receives a copy of the pointer. Inside the function, p = &book rebinds that local copy. The caller's current is left exactly where it was, still pointing at mug.

To actually swap the caller's pointer, the function needs the address of the caller's pointer variable, which is a **Product:

Now the function has the address of current itself, not just a copy of its value. The line *pp = &book writes through pp into current, replacing the pointer it holds. After the call, current points at the new book.

**T gives a function the power to reassign the caller's pointer variable, not just the value behind it. Mutating *T is one job; rebinding T* is another.

Linked List: Updating the Head with **Node

A classic case where **T appears is updating the head of a linked list. Prepending an item or popping the front node both change which node is the head, so a function that does either needs to replace the caller's head pointer, not just mutate a node.

Here's a tiny linked list of product codes. Each node holds a code and a pointer to the next node.

The first call to prepend starts with an empty list (head is nil). prepend needs to set head to a brand new node. A *Node parameter couldn't do that, because the function would only get a copy of the nil pointer and rebinding the copy wouldn't help the caller. A **Node parameter does the job: *head = newNode writes through head and replaces the caller's head variable.

The function takes pp **Node, which is the address of the caller's head variable. To prepend, it builds a new node whose Next is the current head, then writes that new node into head via *pp = newNode. The caller's head now points at the new front of the list.

The same idea works for popFront: it removes the front node and updates the head to point at whatever was second.

Each call to popFront advances the caller's head one step forward. Without **Node, the function would only see a copy of the head pointer and couldn't update the caller's variable. The parentheses around (*head).Code and (*head).Next are necessary because . binds tighter than * in Go's expression grammar.

The Alternative: Return the New Head

Pointer-to-pointer is one way to update a head. The other way, often clearer in Go, is to return the new head from the function and let the caller reassign:

Compare the two designs side by side:

Aspect**Node parameterReturn new head
Function signatureprepend(head **Node, code string)prepend(head *Node, code string) *Node
Caller codeprepend(&head, "BOOK-01")head = prepend(head, "BOOK-01")
Dereferencing inside fn*head = &Node{...}return &Node{...}
Reads like"Go modify my head""Compute the new head and return it"
Aligns with idiomLessMore (Go favors return values)

Most Go codebases prefer the return-value version. It avoids the extra & and * noise, the caller can see clearly that head is being reassigned, and the function signature reads like a value-producing operation rather than an "in-out parameter" trick. Standard library types like container/list hide the head pointer inside a struct entirely, which is the cleanest design when the data structure has more than a couple of operations.

Tree Insert: Updating a Parent's Child Slot

A binary search tree gives a slightly different **T use case. When you insert into a BST, you walk down the tree looking for the right spot. The "right spot" is some node's Left or Right field, currently nil. To plug in the new node, you have to assign to that exact field from inside a recursive function. Passing **Node lets you carry the address of that field, so you can write to it when you find the nil slot.

Notice the first call: insert(&root, p). The root starts at nil, and insert writes through slot to set it. On subsequent calls, the recursion passes &(*slot).Left or &(*slot).Right, which is the address of a child field. When recursion bottoms out at a nil slot, the new node is plugged in via *slot = &Node{...}.

The same algorithm with a "return new subtree root" style avoids **Node:

Same result, no **Node. Each recursive call returns the (possibly new) subtree root, and the caller assigns it back into the right slot (n.Left, n.Right, or the top-level root). This style is closer to functional updates and reads more naturally to most Go programmers.

The **Node version is occasionally favored in performance-critical code because it avoids re-storing the returned pointer at every level of recursion. The difference is microscopic in practice, so unless a benchmark says otherwise, the return-based version is the better default.

Wrapping State in a Struct

A third alternative, often the cleanest when the data structure has several operations, is to wrap the head in a struct and pass a pointer to that struct around. Then a function takes a *List, mutates the struct's Head field directly, and there's no **Node anywhere.

The methods take a pointer receiver, which means they have access to the actual List value. Reassigning l.Head is a normal field assignment through a pointer to the struct. No **T needed, no addresses of pointers, and the operations sit on the type as methods, which most callers find easier to read.

For anything beyond a one-off function, this struct-and-methods pattern is what Go codebases tend to settle on. It scales: adding a Length() method, a Find(code string) method, or an iterator is straightforward, and none of them have to thread **Node through their signatures.

When Not to Use **T

Most of the time, **T is the wrong choice. Before using it, consider whether one of these simpler designs would work:

SituationBetter than **T
Function needs to mutate a valuePass *T, mutate *p
Function needs to replace which value the caller holdsReturn the new pointer, caller reassigns
Data structure has several operations on a head/rootWrap in a struct, use methods
Recursive insert into a treeReturn the (possibly new) subtree root
You want to "pass by reference"Just pass *T. Go doesn't have C++ references; pointers are the closest thing.

There's one situation where **T is the right call without much debate: when the alternative would be more confusing or noticeably slower in a hot path, and a single function (not a whole data structure) needs to mutate the caller's pointer variable. Even then, take a second look. A small struct wrapper is often clearer.

The unsafe and CGO worlds sometimes hand back **SomeCType from C APIs that allocate and return a pointer through an out-parameter. That's interop, not idiomatic Go, and it's the only place you'll see **T regularly in production codebases. For the kind of code most engineers write day to day, sticking to *T plus return values is the right default.

If you find yourself writing **T in a function signature, try rewriting with a return value or a method on a struct first. If neither rewrite reads as cleanly as the **T version, keep the **T. Otherwise, prefer the rewrite.