skip to content

In Go, what is the difference between a value receiver and a pointer receiver on a method?

level: juniorimportance: must knowfreq 86%

answer

  1. the receiver is just another parameter
  2. Go copies every argument, including this one
  3. one form is handed a copy of the struct
  4. func (o Order) versus func (o *Order)
  5. a write to a copy vanishes at return

basics

~20 s

A value receiver gets a copy of the value, so anything the method writes to it is discarded when the method returns. A pointer receiver gets the address of the caller's value, so its writes are visible afterwards.

solid answer

~50 s

A method's receiver is just an extra parameter, and Go passes every parameter by value. With `func (o Order) Add(n int64)` the method body works on a fresh copy of the `Order`, so `o.TotalMinor += n` updates the copy and the caller's order is untouched once the method returns. With `func (o *Order) Add(n int64)` what gets copied is the pointer, not the struct, so the body writes straight through to the caller's `Order`. So the rule is: if the method has to change the receiver, or the type carries state or a resource that must not be duplicated, use a pointer receiver. Value receivers suit small read-only types. Whichever you pick, keep it the same across every method of that type — mixing the two on one type is the usual source of confusion.

code

go · 13 lines
go
type Order struct {
	TotalMinor int64 // amount in minor units, e.g. cents
}

// value receiver: o is a copy, so the caller's Order is unchanged
func (o Order) AddValue(minor int64) {
	o.TotalMinor += minor
}

// pointer receiver: the write lands on the caller's Order
func (o *Order) AddPtr(minor int64) {
	o.TotalMinor += minor
}

go deeper

for a junior

Be ready to state the rule in one sentence and show it: a value receiver works on a copy, a pointer receiver works on the caller's value. Expect to be handed a five-line method that mutates a value receiver and asked what the caller sees.

for a middle

Explain why the copy happens — the receiver is a parameter and Go passes everything by value — and say what is copied in each case: the whole struct versus one pointer word. Be able to justify a choice on size, mutation and resource ownership.

for a senior

Show judgment about the type as a whole: consistency across all its methods, when a returning-a-new-value API beats mutation, and why a value receiver is not immutability and a pointer receiver is not thread safety.

for a principal

Frame the receiver form as part of a type's published contract rather than a per-method detail, and be ready to say what it costs to change your mind once other packages depend on the type.

## The receiver is a parameter A Go method is an ordinary function with one extra parameter written in front of the name: ```go func (o Order) Total() int64 // receiver type Order — a value receiver func (o *Order) Add(minor int64) // receiver type *Order — a pointer receiver ``` Go has exactly one argument-passing rule: everything is passed by value, a copy. The receiver is no exception. What differs between the two forms is *what* gets copied. - **Value receiver `Order`** — the whole struct is copied into the method. The body's `o` is a separate `Order` that happens to start out equal to the caller's. Fields written inside the method are written on that copy, and the copy is thrown away when the method returns. - **Pointer receiver `*Order`** — the *pointer* is copied (a machine word). Both the caller's variable and the method's `o` refer to the same `Order`, so `o.TotalMinor += minor` mutates the one the caller holds. ## The failure this causes The compiler is perfectly happy with a value-receiver method that assigns to its receiver's fields — writing to a local copy is legal code. Nothing warns you. So a checkout state machine written like this compiles, runs, and silently does nothing: ```go type Order struct { Status string TotalMinor int64 // amount in minor units, e.g. cents } func (o Order) ApplyFee(minor int64) { o.TotalMinor += minor // written to the copy, lost at return } ``` Call `order.ApplyFee(250)` and `order.TotalMinor` is unchanged. Change the receiver to `*Order` and the same body works. This is the single most common first-week Go bug for engineers arriving from a language where an object variable is already a reference; there, calling a mutating method on an object just works, and the copy is invisible until you look for it. ## How to choose The practical checklist: 1. **Does the method modify the receiver?** Then it must be a pointer receiver. There is no other option short of returning a new value. 2. **Does the type carry state that must not be duplicated** — a lock, a counter shared with other goroutines, a handle to something that gets closed? Then pointer receivers, so nobody accidentally works on a second copy of that state. 3. **Is the type large?** A value receiver copies every field on every call. For a struct of a few words this is free and may even be faster than chasing a pointer; for a fat struct in a hot loop it is real work. 4. **Is the type small and conceptually a value** — an amount in minor units, a coordinate, a duration-like number? Value receivers read well and let callers treat it like any other number. 5. **Consistency wins ties.** Go's convention is that a type uses one receiver form throughout. If any method needs a pointer, give them all pointers, so the reader of a call site never has to check the declaration to know whether the call mutates. ## Things that are true but often misstated - A value receiver does **not** give you deep immutability. It stops the method from changing the caller's copy of the fields; it does not stop it from writing through any pointer, slice or map the struct contains. - A pointer receiver is **not** automatically faster. It avoids the struct copy but adds an indirection, and it can push the value onto the heap. - A pointer receiver does **not** make the method safe for concurrent use. Sharing one value between goroutines is precisely what makes synchronisation necessary. - The alternative to mutation is to return the new value: `func (o Order) WithFee(minor int64) Order`. That keeps the value semantics and makes the update visible at the call site as an assignment. ## The mental model to keep Read `func (o Order) M()` as `func M(o Order)` and `func (o *Order) M()` as `func M(o *Order)`. Every question about what a method can and cannot change answers itself once the receiver is back in the parameter list where it belongs.

  • Is anything copied at all when a pointer-receiver method is called?
    Yes — the pointer itself is copied, like any argument. That copy is one machine word and it still refers to the caller's value, so writes through it are visible. Reassigning the receiver variable itself inside the method, as in `o = &Order{}`, only changes the local pointer and is lost, exactly like any other parameter.
  • If a method only reads the receiver, does the choice still matter?
    Functionally no, but two things push the decision. A value receiver copies the whole struct on every call, which matters for large types or hot paths. And the convention that one type uses one receiver form matters more than either: if any method needs a pointer, give the read-only ones pointers too so callers never have to check.
  • How do you express an update without a pointer receiver?
    Return a new value: `func (o Order) WithFee(minor int64) Order` builds a modified copy and hands it back, and the caller writes `order = order.WithFee(250)`. The mutation becomes an assignment the reader can see. It suits small immutable-style types; it is a poor fit for a type that owns a resource or is shared.

saying these in an interview costs you the question

  • Says a value receiver can change the caller's struct
  • Thinks Go passes structs by reference like object references elsewhere
  • Believes the compiler picks the receiver form automatically
  • Claims pointer receivers are always faster than value receivers
  • Says a value receiver makes the type immutable
  • Assumes a pointer receiver makes the method concurrency-safe