Last Updated: May 22, 2026
The mechanics of pointers fit on a few pages, but knowing when to use one is where most engineering judgment lives. A program full of unnecessary pointers reads like a leaky C codebase and runs slower than value-based code would. A program that avoids pointers when they're called for ends up with silent mutation bugs, copied mutexes, and APIs that lie about what they do. This chapter pulls together every reason to use a pointer, every reason not to, and the patterns the Go standard library uses so you can copy the right defaults.
Before writing *Product or Product, walk through five questions in order. The first one that answers "yes" picks the pointer. If none of them do, default to a value.
Everything else in this chapter is the justification for those five lines, the patterns that fall out of them, and the anti-patterns that show up when someone skips them.
The order matters. Mutation is the most common reason and the strictest requirement, since the compiler can't fix a missing pointer on a mutating function. Identity and nullability come up less often but have no value-semantic substitute. Size is a real concern but easy to get wrong if you eyeball it. And "none of the above" is the most underused answer in Go code: small types passed by value are cheap, clear, and immune to a whole class of bugs.
The flowchart is just the five questions drawn out. Run any "should this be a pointer?" decision through it, and the answer falls out. The rest of the chapter unpacks each question with examples and the cases where the rule bends.
If a function or method needs to modify the caller's value and have that change persist, it must take a pointer. There is no value-semantic alternative. The design question is what counts as "needing to mutate."
A method named Restock, AddItem, ApplyDiscount, or MarkShipped is mutating by its name. So is anything with Set, Update, Reset, Push, Pop, or Append in its identifier. The intent is in the verb. If the verb describes a change to state, the receiver is a pointer.
Both methods change the order in place. With value receivers, the assignment would happen on a throwaway copy and the order would stay empty and PLACED. The pointer is what makes the change visible to the caller.
The same rule applies to standalone functions. A function that takes a struct by value and tries to modify it is writing to a local copy. If the function's purpose is to mutate, take a pointer:
clearCart takes *Cart because its job is to reset the field. Passing cart by value would work syntactically, but the assignment c.Items = nil would die with the local copy and the real cart would keep its items.
Cost: the pointer itself is one machine word (8 bytes on a 64-bit system). Passing a pointer doesn't free you from copying entirely; the pointer still gets copied. But the cost is fixed, regardless of how big the struct is.
Go has no built-in optional type. The standard way to express "this field may or may not be present" is a pointer, where nil means "not set."
Consider a Product with an optional discount. Not every product is on sale. If the field is a Discount value, you have to use some sentinel like Percent: 0 to mean "no discount", and the type doesn't tell you which is which. A pointer makes the absence explicit:
regular.Discount is nil. onSale.Discount points to a real Discount value. Code that consumes a Product checks the pointer before dereferencing, and the meaning of "no discount" is unambiguous. There's no special sentinel to remember.
The same pattern fits any field where "absent" is a real state distinct from "zero": an optional ShippedAt *time.Time field on an order (nil before shipment), an optional ParentCategory *Category field (nil for top-level categories), an optional BillingAddress *Address (nil if the customer reuses the shipping address).
The pattern has a cost, though. Every read of an optional pointer field needs a nil check, and forgetting one is a panic. For fields where the zero value naturally means "not set" (an empty string for a missing nickname, a zero time.Time for never-shipped), a value field is often cleaner. The pointer is for cases where the zero value is itself a valid choice and you need a third state.
JSON encoding follows the same pattern. A Discount *Discount field with a json:"discount,omitempty" tag will be omitted from JSON output when nil, which matches the API conventions of most modern services. A value-typed field can't express that distinction.
Copying a small struct is effectively free. Copying a large one on every call adds up. When a struct is big enough that the copy cost matters, prefer a pointer to avoid the copy.
The Go community has no fixed threshold for "large." A useful heuristic is: if the struct has more than four or five fields, or contains any field with a substantial body (a long string, a large array, an embedded struct of its own), prefer a pointer. The Go team's own style guide phrases it as "if in doubt, use a pointer," which biases toward pointers for anything non-trivial. Some performance-focused engineers cite "fits in a cache line" (64 bytes on most CPUs) as a softer boundary, but the Go runtime's escape analysis and inlining usually make the exact threshold less important than the consistency of the choice.
A small struct that fits the value-receiver pattern:
Money has two small fields. Passing it by value is cheap and the immutable feel matches money's semantics: you don't mutate $5, you produce a new value from $5 plus $3.
A larger struct that fits the pointer pattern:
Product has twelve fields, including multiple string fields and two slices. Passing it by value to every method or function would copy all of that on every call. A pointer copies eight bytes regardless.
Cost: copying a struct copies every field, including the slice headers and string headers inside. For Product above, the copy is roughly 96 bytes or more depending on layout. That's still fast on modern hardware, but in a hot path called millions of times, the cost shows up in benchmarks.
The "large struct" rule has a caveat. The Go runtime's escape analysis sometimes makes pointer access slower than value access for very small structs, because the pointer forces the value onto the heap when the compiler can't prove otherwise. For tiny structs like Money or Point{X, Y}, a pointer is often slower than a value, not faster. Don't use a pointer "for performance" on a small struct without measuring. The decision tree above puts mutation, optionality, and identity before size for exactly this reason: size alone is the weakest justification.
The diagram shows the trade-off. On the value path, every call copies the full struct into the function's stack frame and discards it on return. On the pointer path, only the eight-byte pointer is copied. The pointer is cheaper when the struct is large, but the indirection itself has a cost (one extra memory load) that can dominate when the struct is tiny.
Two value-typed Product variables can be compared for "same data" using ==, but two *Product variables can be compared for "same instance." When the program cares about which is which, pointers are the only way to express that distinction.
A simple example: a cart holds references to products. If two cart entries refer to the same product, updating the product's price in one place should update it for both. With value types, you'd have two independent copies and would have to keep them in sync manually:
The two copies are stuck at the old price. They're separate Product values that started with the same field values, but they have no ongoing relationship to the original.
With pointers, both cart entries refer to the same underlying product:
All three pointers refer to the same Product. A change through any of them is visible through all of them. This is shared ownership, and it's the natural way to model anything that has identity in your domain: a user account, an open order, a product in an inventory, a session.
Identity comparison also becomes meaningful. With value types, book1 == book2 returns true whenever every field is equal, even if the two were created independently. With pointers, book1 == book2 returns true only when both point to the same memory:
a and b compare equal by value because all their fields are equal. pa and pb compare unequal because they point at different addresses, even though the values at those addresses match. pa and pc compare equal because they're aliases for the same memory.
Shared ownership has a cost that's worth naming. Multiple owners can mutate, which makes it harder to reason about who changes what when. The standard library's sync.Mutex and sync/atomic package exist partly to handle this. For single-threaded code, shared ownership is fine. For concurrent code, you'll need to think about synchronization.
A type's method set determines which interfaces it satisfies. A value type T has the methods declared on T, while a pointer type *T has the methods declared on both T and *T. This asymmetry means a type with any pointer-receiver methods can only satisfy certain interfaces through its pointer form.
A practical example: anything implementing io.Writer needs a Write([]byte) (int, error) method, which almost always wants a pointer receiver because it mutates internal state. So bytes.Buffer's Write method is declared on *bytes.Buffer, and you pass &buf to functions that want an io.Writer, not buf itself.
The function takes an io.Writer, and *bytes.Buffer satisfies it because Write is declared with a pointer receiver. Passing buf (the value) instead of &buf would fail to compile, because the value form of bytes.Buffer doesn't have Write in its method set.
This is rarely a reason to use a pointer on its own. The pointer is usually already justified by mutation (rule 1). But it's worth knowing: if your type implements an interface, and the interface's contract involves mutation, your type's methods will be on the pointer, and your callers will pass pointers.
The short version of the receiver consistency rule: once any method on the type needs a pointer receiver, declare every method on the type with a pointer receiver. The type's method set stays unified, and interface satisfaction stays predictable.
The flip side of the framework is just as important. Pointers cost something, and there are real cases where reaching for one makes the code worse, not better.
Small types. A pointer to a small struct, a single int, or a bool adds indirection that the original value doesn't need. The pointer dereference is at minimum one extra memory load on every access. For a single int, that's slower than just passing the value. Use a pointer on a small type only if you have a specific reason (mutation, optionality, identity). Otherwise, pass it by value.
The first signature obliges every caller to declare a variable, take its address, and check it after. The second signature is just a function that answers a question, which is the natural shape.
Immutable data. If a value never changes after creation, there's no reason to share a pointer to it. Just pass it by value. Strings are the canonical example: a Go string is already a small header (pointer plus length, 16 bytes on 64-bit) and the bytes it points to are immutable. Passing *string is almost always wrong. The same logic applies to any read-only configuration value, lookup table, or constant.
Hot loops and cache locality. When iterating millions of times over a slice, pointer-chasing can hurt performance. Each dereference is a memory load that might miss the cache. A slice of values has its elements packed contiguously, which is cache-friendly. A slice of pointers points to objects scattered across the heap, which often isn't. This is a real cost, but it only matters in genuine hot paths, and you need to measure to know whether it applies. Don't preemptively switch to value slices "for performance" without a benchmark showing it matters.
Premature optimization. "I'll use a pointer for speed" is the most common bad reason to use a pointer. Pointer access has its own costs (escape analysis, heap allocation, indirection, pressure on the garbage collector). For small structs, value semantics are often faster. The Go runtime is good at keeping stack-allocated values fast. Unless you have a profile or benchmark showing a copy is the bottleneck, the size rule (rule 3) shouldn't lead you to pointers for small types.
Confusing ownership. When it's unclear who can mutate a value, pointers make the confusion worse. If a function takes a *Cart, can the caller still trust the cart's state after the call? If three goroutines hold a *Order, who decides when the order is final? Value semantics force a copy, which makes ownership obvious: each function gets its own. When ownership is genuinely shared, a pointer is appropriate, but make sure shared ownership is what you want, not what you're falling into by accident.
The diagram compares the two ownership models. With value semantics, every function has its own copy, and the only way one function affects another is through a return value. With pointer semantics, all three functions can write to the same underlying value, and any function can be the one that changes it. The pointer version is more flexible but harder to reason about. Pick it when you need it, not by default.
The Go standard library has set patterns that are worth following. They help code fit with the rest of the ecosystem.
**Constructor functions returning *T.** When a type's methods are on the pointer (because it mutates, holds a mutex, or is large), the constructor returns the pointer too. This makes the type's intended use obvious at the call site.
NewCart returns *Cart, so the caller gets a value that already has the full method set. The standard library uses this everywhere: bytes.NewBuffer, bufio.NewReader, http.NewRequest, sql.Open, tls.NewListener. The convention is so strong that returning a value type from a New function is a signal to the reader that the type is meant to be passed by value.
**Builder pattern returning *T.** A method that's part of a fluent chain returns the receiver, and for types with pointer receivers, that means returning the pointer. Each step modifies the same underlying value and returns it for the next step.
Each builder method mutates the builder and returns *OrderBuilder, which lets the calls chain. The final Build() returns an immutable value type, since the order is meant to be passed around afterward, not mutated.
Pointers in the standard library: why each one is a pointer. The standard library is full of *Something types. Each one is a pointer for a specific reason from the framework above.
| Type | Why it's a pointer |
|---|---|
*sql.DB | Holds a connection pool and sync.Mutex (size and identity, plus must not be copied) |
*http.Request | Large struct with many optional fields, modified by middleware (size and mutation) |
*http.Response | Holds a Body reader that's drained as you read (mutation, identity) |
*bytes.Buffer | Holds a growing byte slice and read offset (mutation) |
*os.File | Wraps a file descriptor that has identity (identity, mutation on close) |
*sync.Mutex | Must not be copied; locking a copy is a bug (identity) |
*log.Logger | Holds output writer and configuration (size, shared ownership) |
The pattern is consistent: the standard library uses pointers when one or more of the five reasons applies. It doesn't use pointers for time.Time, net.IP, big.Int (mostly), or other small, value-like types where the reasons don't apply.
Receiver consistency across the type. Once a type uses pointer receivers anywhere, every method on the type should use a pointer receiver. The standard library is rigorous about this. Browse bytes.Buffer: every method takes *Buffer. Browse sql.DB: every method takes *DB. Browse http.Request: every method takes *Request. The consistency makes the type's method set unified and predictable.
Some pointer uses look reasonable at first but almost always indicate a misunderstanding. These are the patterns to recognize and avoid.
Pointer to a slice. Slices are already reference-like. A slice header contains a pointer to the backing array, plus length and capacity. Passing []Product already shares the backing array with the caller; passing *[]Product adds a layer of indirection for almost no benefit.
The only case where *[]Product is justified is when the function genuinely needs to replace the caller's slice header (because append reallocated), and you want the caller to skip the s = append(...) reassignment. Even then, returning the new slice is clearer. Almost every "I need to modify a slice" use case is better served by a method on a struct that owns the slice.
Pointer to a map. Maps are also reference-like. A map value is a pointer to the runtime's hash table. Passing map[string]int to a function lets the function add, modify, and delete entries; the caller sees all of it. Passing *map[string]int is unnecessary and unidiomatic.
The only case where a pointer to a map makes sense is when the function might need to replace the entire map with a new one (which is rare and almost always better handled by returning a new map or wrapping the map in a struct).
Pointer to a channel. Channels are already reference-like, just like slices and maps. A channel value is a pointer to the runtime's channel structure. Passing chan int shares the channel with the caller. Passing *chan int is almost always a mistake.
Pointer to an interface. An interface value is itself a two-word structure: a type pointer and a value pointer. The value pointer is doing the pointer work already. Adding another pointer (*io.Writer) is redundant and breaks several patterns:
The *io.Writer version forces callers to take the address of their writer variable, and it doesn't let them pass things like os.Stdout directly (since os.Stdout is a *os.File, not addressable as *io.Writer). The idiomatic version accepts any io.Writer and works with anything implementing the interface.
The one genuine use case for *SomeInterface is when you need to swap one implementation for another at runtime, observable to the caller. It's so rare that whenever you see it, you should look hard at the design first. There's almost always a better shape.
Pointer to a string. Go strings are already small (16 bytes: a pointer plus a length). The underlying bytes are immutable, so there's no mutation to enable through a pointer. The only reason to use *string is to represent optionality, like *Discount from rule 2. For "this string might be missing," a *string field is fine. For passing strings around to functions, plain string is always better.
Pointer "for performance" on small types. Reaching for a pointer to a small struct (a Point, a Money, a Color) under the assumption that pointers are always faster is a classic mistake. For tiny structs, the value form is at least as fast, often faster, and immune to escape-analysis surprises. Profile before you optimize.
A real e-commerce checkout example shows the framework in action. Each pointer or value choice in the code below is justified by one of the five reasons.
The code has six pointer-using decisions, each justified by a different reason from the framework:
| Choice | Reason |
|---|---|
Money is a value type | Rule 5: small, immutable, no need for pointer |
Product.Discount is *Discount | Rule 2: optional field, nil means "no discount" |
Product methods use pointer receivers | Rule 1: mutation (Restock); consistency forces it on FinalPrice too |
CartItem.Product is *Product | Rule 4: shared ownership, restocking one product affects every cart that references it |
Cart methods use pointer receivers | Rule 1: mutation (AddItem, Place); Rule 3: Cart is large |
Cart.PlacedAt is *time.Time | Rule 2: optional, nil means "not yet placed" |
Every pointer in the code has a reason. Nothing is a pointer "for speed" or "just in case." When the type system gives you the right defaults, the code reads naturally and the bugs the framework was designed to prevent (silent mutation, missing-vs-zero confusion, identity loss) don't show up.