11.2 Value Receivers vs Pointer Receivers: Mutation and Copying
Alright, let’s get into the weeds on this one. The choice between value and pointer receivers isn’t just academic; it dictates whether your code will be efficient, correct, or a complete head-scratcher when it fails. At its core, this whole debate boils down to one question: are you trying to change the state of the thing you’re calling the method on, or are you just using its current state?
Think of a value receiver like getting a photocopy of a document. You can scribble all over your copy, highlight things, tear it up—the original remains pristine. A pointer receiver is like someone handing you the original document and a pen. Your changes are permanent and everyone else sees them.
The Golden Rule of Mutation
Here’s the rule I live by, carved from years of debugging nonsense: If a method needs to mutate its receiver, you MUST use a pointer receiver. There is no negotiation on this. The universe will punish you for trying to mutate a value receiver. Let’s see why.
type BankAccount struct {
balance float64
}
// Value receiver method (The Wrong Way™)
func (b BankAccount) DepositWithValue(amount float64) {
b.balance += amount // This is a futile act of rebellion.
}
// Pointer receiver method (The Correct Way)
func (b *BankAccount) DepositWithPointer(amount float64) {
b.balance += amount // This actually works.
}
func main() {
account := BankAccount{balance: 100}
account.DepositWithValue(50)
fmt.Println(account.balance) // Output: 100. Wait, what?!
account.DepositWithPointer(50)
fmt.Println(account.balance) // Output: 150. Thank you.
}
See that? DepositWithValue receives a copy of the account struct. It increments the balance on that copy, and then the copy is immediately discarded, achieving absolutely nothing. It’s the most polite and useless method ever written. DepositWithPointer receives the address of the original struct, so it can go directly to the source of truth and update it. This isn’t just a “best practice”; it’s the only way to make it work.
The Performance Question (Or, Stop Copying That 4MB Struct)
Even if you’re not mutating state, you might still want a pointer receiver. Why? Performance. Go is an “pass-by-value” language, meaning when you call a method on a value, the entire struct is copied into the method. For a tiny struct with two int fields, who cares? For a massive struct with a 4MB embedded image? You should care.
type TinyStruct struct {
a, b int
}
type MassiveStruct struct {
data [1 << 20]byte // 1 MB of data. Yikes.
id int
}
func (t TinyStruct) SizeTest() {} // Inexpensive copy.
func (m MassiveStruct) SizeTest() {} // Horrifically expensive copy.
func (t *TinyStruct) PtrSizeTest() {} // Cheap, copies an address.
func (m *MassiveStruct) PtrSizeTest() {} // Also cheap, copies an address.
Using a pointer receiver here is just good sense. You’re copying a mere pointer (8 bytes on a 64-bit machine) instead of the entire, monstrous payload. The Go compiler is smart, but it’s not “magically avoid a megabyte copy” smart.
The Bizarre (But Consistent) Case of Implicit Dereferencing
Here’s where Go shows off its pragmatic, sometimes confusing, side. The language tries to save you from pointer-related typing cramps. If you have a value v of type T and a method with a pointer receiver *T, Go will happily let you call v.PtrMethod(). It automatically takes the address for you ((&v).PtrMethod()). This feels weird but it’s incredibly convenient.
The reverse is also true! If you have a pointer p of type *T and a method with a value receiver T, Go will dereference the pointer for you to call p.ValueMethod() (it does (*p).ValueMethod()).
func (b BankAccount) Balance() float64 { // Value receiver
return b.balance
}
func (b *BankAccount) Withdraw(amount float64) { // Pointer receiver
b.balance -= amount
}
func main() {
account := BankAccount{balance: 200}
accountPtr := &account
// These are all valid and equivalent. Go handles the conversion.
fmt.Println(account.Balance()) // Value calling value method.
fmt.Println(accountPtr.Balance()) // Pointer calling value method. Go does (*accountPtr).Balance()
account.Withdraw(50) // Value calling pointer method. Go does (&account).Withdraw(50)
accountPtr.Withdraw(50) // Pointer calling pointer method.
}
This syntactic sugar is why you can often define all of a type’s methods using pointer receivers and then seamlessly work with both values and pointers of that type. It’s a common and often sensible pattern, especially for structs that have any state at all.
So, Which One Should You Use?
Stop thinking and use this flowchart:
- Does the method need to modify the receiver? -> Use a pointer receiver (
*T). - Is the receiver a large struct (or array, or likely to become large)? -> Lean towards a pointer receiver for efficiency.
- Is the receiver a primitive type (like
int,string), a tiny struct, or something you want to ensure is never modified? -> A value receiver (T) is safe and conceptually correct. - For consistency: If some methods of a type need a pointer receiver, it’s often cleaner to just give all methods pointer receivers, even the read-only ones. It avoids confusion about the type’s intended usage.
The one caveat to this sugar-coated world is that interface satisfaction is not sugar-coated. A type T has methods with value receivers; it satisfies an interface. A type *T has those methods too (through the sugar), so it also satisfies the interface. But if you define methods only on *T, then T itself does not satisfy the interface. Remember that. It’s the one time the convenience veil drops and the actual method set matters. But that’s a topic for the next section. For now, just remember: if you want to change it, point to it.