You own an exported Go type other teams import. How do you decide pointer versus value receivers before release?
answer
- it is a promise to importers, not a detail
- one form advertises that copying is fine
- you cannot quietly flip it after release
- resource, mutability, size, consistency
- put copy-safety in the doc comment
basics
~20 sTreat the receiver form as part of the published contract, not a per-method detail. Value receivers tell importers the type is safe to copy; pointer receivers tell them it has identity and must be shared. Decide once, apply it to every method, and document it.
solid answer
~50 sThe receiver form is a promise to every package that imports the type. All-value receivers say 'this is a value: copy it, pass it around, compare it, use it as a map key' — and callers will do exactly that. All-pointer receivers say 'this thing has identity, aliasing is intended, and you must reason about who holds it'. Once the type is released you cannot quietly flip that: importers' code and their assumptions about copying are built on it. So I decide before release, on a few facts about the type — does it carry mutable state or own a resource; must it never be duplicated; is it small and conceptually an amount or an identifier; is it large enough that copying costs. Then I apply the answer to all its methods for consistency, write the copy-safety rule into the doc comment, and treat the decision as an API review item, because it is the kind of call a reviewer can and should overrule before it ships.
go deeper
Know that the receiver form is visible to everyone who uses the type, and that a package's exported types normally stick to one form throughout rather than mixing.
Explain what each form implies for callers — copying and comparing versus sharing and nil handling — and be able to justify the choice for a specific type from its state and size.
Show that you decide it once for the whole type, document copy-safety in the doc comment, and can name what breaks for existing users if the form changes after release.
Own it as a release-gate decision: judged against importer usage rather than implementer convenience, on the API review checklist, with a stated migration route — new type or new major version — when the answer has to change.
## Why this is a contract question, not a style question Inside one package, changing a method from `func (o Order) M()` to `func (o *Order) M()` is a small edit. On an exported type that other teams import, it is a change to the shape of the thing they built on. That is what makes the decision worth a deliberate call before release, and what makes it a legitimate item for an API reviewer to challenge. The reason is that the receiver form is read as a statement about the type, whether or not you meant it that way: - **All value receivers** tell importers the type is copy-friendly. They will store it by value in their own structs, pass it through channels, put it in slices and maps, and hold two copies without thinking about it. If the type later acquires internal state where duplication is wrong — a counter, a cache, a handle — every one of those uses becomes a latent bug in *their* code, not yours. - **All pointer receivers** tell importers the type has identity: there is one of these and everybody shares it. They will pass `*T` around, put nil checks in, and reason about aliasing. In exchange they accept that constructing it means a constructor function, and that concurrent use needs their own discipline or yours. ## The facts that decide it I weigh, in roughly this order: 1. **Does the type own something that must not be duplicated?** A lock, a live connection or handle, a counter other goroutines observe, anything with a `Close`. If yes, pointer receivers, and say so in the doc comment: 'a Client must not be copied after first use.' 2. **Is the type mutable through its API?** If any method changes state, all methods take pointers. Mixing is the worst outcome: callers can no longer tell from the type which calls mutate. 3. **Is it conceptually a value?** An amount in minor units, an identifier, a small coordinate, an immutable configuration snapshot. Value receivers make it behave like the built-in types callers already reason about — copyable, comparable, usable as a map key when its fields are comparable. 4. **Size and call frequency.** A large struct copied on every read method in a hot path is real cost. This is the weakest of the four inputs and should not override the semantic ones; measure before it decides anything. 5. **Consistency across the type**, always. One form for every method of the type, so the reader of a call site needs to know only the type. ## What it costs to change your mind later Pre-release, this is free. After release, flipping value receivers to pointer receivers changes what callers can do with values they already hold, and their code was written against the previous answer. In an ecosystem where you cannot redeploy your importers, that is a breaking change even when some of it still compiles — which is the dangerous variety, because the failure surfaces as behaviour rather than as a build error. The honest options once you are in that position are to add a new type with the semantics you now want and migrate importers deliberately, or to publish a new major version of the module and let importers move on their own schedule. Both cost more than the ten minutes of thought before release, which is the entire argument for treating this as a release-gate item. ## The organisational move Make the decision explicit rather than emergent. In practice that means three things. Put copy-safety in the doc comment of every exported type — one sentence saying whether a value may be copied — so importers do not have to infer it from receiver forms. Put 'receiver form and copy-safety' on the API review checklist for new exported types, alongside naming and error shape. And when a type's receiver form is genuinely contested, decide it against the *importers'* likely usage, not the implementer's convenience: the implementer changes their code once, the importers change theirs never. ## The tradeoff you should be willing to state Defaulting an entire codebase to pointer receivers is the safe answer and a real loss. It gives up the ability to treat small domain types as values, drags nil-handling into APIs that never needed it, and makes every exported type feel like a resource. Defaulting to value receivers is cleaner and fails badly the first time a type grows internal state. The defensible position is per type, decided from the four facts above, applied consistently, documented, and reviewed — and a willingness to say which way you leaned and why is what an interviewer is listening for.
- A released type uses value receivers and now needs internal state that must not be duplicated. What do you do?Do not quietly flip the receivers — importers already copy the value and their code was written on that promise. The honest routes are a new type with the semantics you want plus a documented migration, or a new major version of the module so importers move on their own schedule. Both are deliberate, visible changes.
- Why not simply default every exported type to pointer receivers and be safe?It costs real expressiveness. Small domain types — an amount, an identifier, a configuration snapshot — read better and are safer as values that callers can copy and compare. Blanket pointers also push nil handling into APIs that never needed it and make every type look like a resource with a lifecycle.
- How do you communicate the decision to importers who never read the declarations?In the exported type's doc comment, in one sentence: whether a value of the type may be copied after first use, and whether the zero value is usable. That is the line importers actually read, and it survives refactors of the individual methods far better than an inference from receiver forms.
- Where does this decision belong in the release process?On the API review for any new exported type, next to naming and error shape, because it is cheap to change before release and expensive after. Reviewing it there also forces the question of who the importers are and how they will hold the value, which is the input that should actually decide it.
saying these in an interview costs you the question
- Treats the receiver form as personal style rather than API surface
- Mixes value and pointer receivers on one exported type
- Plans to change receivers later without a version story
- Defaults everything to pointers without weighing the cost
- Never documents whether an exported value is safe to copy