Golang Receiver Mistake That Silently Destroys Your Struct
You wrote a method, it compiles, tests pass — and the struct still hasn’t changed. Or you implemented an interface, and Go tells you it isn’t implemented. These aren’t random bugs. They are receiver type mistakes, and they hit almost every developer who comes to golang from Python, Java, or JavaScript.
The problem isn’t the syntax. The problem is that golang gives you no warning when you do it wrong.
TL;DR: Quick Takeaways
- A value receiver gets a full copy of the struct — mutations inside the method vanish after the call returns.
- A pointer receiver operates on the original struct in memory — mutations persist and large structs aren’t copied on every call.
- Mixing pointer and value receivers on the same struct breaks interface satisfaction in non-obvious ways.
- Go does not automatically convert between pointer and value receivers when checking interface compliance — only when calling methods directly on a variable.
What Is a Value Receiver in Go
When you define a method with a value receiver, golang passes a complete copy of the struct into the method. Every field, every nested value — copied. The method works on that copy. When the method returns, the copy is discarded and the original struct in your calling code is completely untouched. This is not a bug — it is intentional Go design. The issue is that nothing tells you it happened.
type Counter struct {
count int
}
// value receiver — receives a copy of Counter
func (c Counter) Increment() {
c.count++ // modifies the copy, not the original
}
func main() {
c := Counter{count: 0}
c.Increment()
fmt.Println(c.count) // prints 0, not 1
}
This compiles without a warning. Go will not tell you that your mutation did nothing. The method is syntactically valid — it just doesn’t do what most developers expect on first contact with golang. That silence is what makes value receiver bugs expensive to find in a real codebase.
When value receivers are actually correct
Value receivers are not just a footgun — they have a legitimate purpose. If a method only reads from the struct and modifies nothing, a value receiver is semantically correct. It signals to the caller that this is a pure read. Small structs — two to four fields of primitive types — copied by value have negligible performance overhead. The golang standard library uses value receivers on time.Time for exactly this reason: the struct is small and all methods are read-only. If your type is meant to be immutable, value receivers reinforce that contract.
Where value receivers silently break your logic
The dangerous case is a method that looks like it mutates state — incrementing a counter, appending to a slice, flipping a flag — but sits on a value receiver. Nothing breaks at compile time. The mutation runs on the copy, the copy is thrown away, and your struct is exactly as it was before the call. In production this shows up as a counter that never increments, a cache that never fills, or a state machine that never transitions. These bugs are particularly nasty because the logic looks correct when you read it — you have to understand the receiver type to see what’s wrong.
What Is a Pointer Receiver in golang
A pointer receiver passes the memory address of the struct to the method instead of copying it. The method operates directly on the original data. Any field mutation persists after the method returns, visible to the caller immediately. Beyond correctness, pointer receivers matter for performance: a struct with 20 fields copied on every method call creates real allocation pressure in a hot path. With a pointer receiver, only 8 bytes — the pointer receiver in golang itself on a 64-bit system — are passed, regardless of struct size.
type Counter struct {
count int
}
// pointer receiver — receives address of Counter
func (c *Counter) Increment() {
c.count++ // modifies the original struct
}
func main() {
c := Counter{count: 0}
c.Increment()
fmt.Println(c.count) // prints 1
}
The only syntactic difference is the asterisk before the type name in the receiver. The behavioral difference is total. Now c.count is mutated in place. Golang automatically takes the address of c when you call c.Increment() on an addressable variable, so you don’t need to write (&c).Increment() explicitly. That convenience is useful but it also obscures what’s actually happening under the hood.
Where Go’s Simplicity Breaks Down: 4 Non-Obvious Problems at Scale. Go has become a go-to choice for backend engineers thanks to its clear syntax, fast compilation, and approachable concurrency model. Yet, Go performance issues at...
[read more →]Pointer receivers and nil safety
One non-obvious consequence of pointer receivers: a method with a pointer receiver can be called on a nil pointer without an immediate panic — as long as the method doesn’t dereference the pointer. This can be intentional (implementing a nil-safe String() method, for example) or catastrophic (calling a method that accesses fields on a nil struct). Unlike Java’s NullPointerException, golang gives you no stack trace until you actually dereference. A nil pointer receiver that reaches field access panics at that line, not at the call site, which makes the error harder to trace back in deep call chains.
Pointer vs Value Receiver: The Practical Decision
Choosing between pointer and value receiver is not a style preference — it has concrete consequences for correctness, performance, and interface satisfaction. The golang team’s own code review guidelines give clear direction here, and the rules are simpler than most junior developers expect.
| Situation | Value Receiver | Pointer Receiver |
|---|---|---|
| Method modifies struct fields | Wrong — mutations lost | Correct |
| Struct is large (10+ fields) | Copies on every call | Correct — 8 bytes passed |
| Method is read-only | Correct | Also fine, but less expressive |
| Struct contains a mutex or sync type | Wrong — mutex copied, useless | Correct |
| Struct is used as interface value | Only satisfies interface from value | Satisfies interface from both pointer and value |
The consistency rule nobody tells you about
If any method on a struct uses a pointer receiver, all methods on that struct should use pointer receivers. This is not a hard compiler rule — golang will let you mix them. But mixing breaks interface satisfaction in ways that are genuinely difficult to debug, and it creates an inconsistent API surface where callers cannot predict whether they need a pointer or a value. The Go code review comments document explicitly recommends consistency across all methods of a type. In practice: decide once per struct, then stick to it.
When your interface implementation silently breaks
This is where the real damage happens. If you define an interface and implement it with a pointer receiver, only a *YourStruct satisfies the interface — not a YourStruct value. Golang enforces this at compile time when the type is used directly, but if the struct is passed as interface{} or through a generic function, the failure can surface at runtime. The error message — does not implement interface (method has pointer receiver) — is clear enough once you know what it means, but it’s baffling the first time you see it because your method is right there in the code.
type Stringer interface {
String() string
}
type User struct {
name string
}
// pointer receiver — only *User implements Stringer
func (u *User) String() string {
return u.name
}
func printIt(s Stringer) {
fmt.Println(s.String())
}
func main() {
u := User{name: "Alice"}
printIt(u) // compile error: User does not implement Stringer
printIt(&u) // works
}
The fix is one ampersand. But the mental model that prevents the bug in the first place is understanding that in Go, the method set of T contains only value receivers, while the method set of *T contains both value and pointer receivers. A pointer type always has more methods available than the corresponding value type.
Practical Go Interfaces: Best Practices to Prevent Overengineering You started with good intentions — a clean service layer, interfaces everywhere, a folder structure that would make Uncle Bob proud. Six months later, navigating your own...
[read more →]Common Mistakes Juniors Make With Receivers
Beyond the basic mutation bug, there are three receiver mistakes that show up repeatedly in golang code reviews — all of them invisible at compile time, all of them painful to debug at runtime.
Copying a struct that contains a mutex
If your struct embeds a sync.Mutex or sync.RWMutex and you use a value receiver, the mutex is copied along with every other field. A copied mutex is a separate mutex — locking it does nothing to protect the original struct. Two goroutines can now access the original simultaneously while both think they hold the lock. The go vet tool catches this specific mistake and flags it as assignment copies lock value. Run go vet ./... before every commit — this is one of several races it catches that the compiler won’t.
Appending to a slice field
This one catches developers who know about the mutation problem but think slices are special. They aren’t. If you append to a slice field inside a value receiver method, the local copy’s slice header is updated — new length, possibly new backing array — but the original struct’s slice header is unchanged. The original struct still points to the old backing array with the old length. Append on a value receiver is always a bug if you intend the caller to see the new elements.
Inconsistent receivers breaking interface compliance at scale
In a small codebase, mixed receivers are annoying. In a large one, they create a situation where the same struct satisfies some interfaces only as a pointer and others only as a value, and the rules for which is which are scattered across dozens of files. Refactoring becomes treacherous. The fix at scale is the same as the fix at the start: pick one receiver type per struct and never deviate. In most real golang services, that means pointer receivers almost everywhere — the cases where a pure value receiver is genuinely better are the minority.
FAQ
Can I mix pointer and value receivers on the same struct in golang?
Technically yes — Go will compile it. But mixing pointer and value receivers on the same type creates an inconsistent method set and breaks interface satisfaction in non-obvious ways. If a struct has even one pointer receiver method, the full interface is only satisfied by a pointer to that struct, not a value. The golang code review guidelines explicitly recommend using one receiver type consistently across all methods of a given type. The only exception is when you’re implementing an interface like fmt.Stringer that you have no control over, and one specific method genuinely needs different semantics.
Why doesn’t Go warn me when a value receiver mutation has no effect?
Because golang has no way to know whether the mutation was intentional or a mistake. Value receivers are a legitimate feature — pure read methods on small structs are a valid use case. The compiler can only verify syntax and type correctness, not semantic intent. The go vet tool catches some specific cases like copying a mutex, but silent value mutation is not in its scope. This is one area where Go’s design philosophy — minimal magic, explicit behavior — works against beginners. The language trusts you to know what you’re doing.
Does golang automatically dereference pointers when calling methods?
Yes, but only for method calls on addressable variables, and only in one direction. If you have a value c and call a pointer receiver method, golang automatically takes &c for you. If you have a pointer p and call a value receiver method, Go automatically dereferences it. This convenience works in direct method calls. It does not work in interface satisfaction checks — the compiler checks the method set of the exact type you’re assigning, not what could theoretically be derived from it.
Go Garbage Collector Internals: Mastering Performance Beyond GOGC=off Go's garbage collector is the engine under the hood. Most engineers ignore it until P99 spikes start killing production SLAs — and by then, the heap is...
[read more →]Is a value receiver faster than a pointer receiver in golang?
For small structs — roughly under 64 bytes — value receivers can be marginally faster because the value lives on the stack and avoids a pointer indirection. For anything larger, pointer receivers win significantly. A struct with 10 string fields (each 16 bytes on a 64-bit system) costs 160+ bytes to copy on every value receiver call. In a tight loop or a high-throughput HTTP handler, that adds up fast. The golang team’s internal benchmarks show that for structs exceeding 3–4 machine words, pointer receivers consistently outperform value receivers. When in doubt, benchmark with go test -bench rather than guessing.
What happens if I call a pointer receiver method on a nil pointer?
The method is called — golang does not panic at the call site. The panic happens only when the method body tries to dereference the nil pointer, typically when accessing a field. This means you can write nil-safe methods that explicitly check for nil before accessing any fields, which is a legitimate pattern for implementing default behaviors. However, if you don’t intend nil-safety and just accidentally pass a nil pointer, the panic will appear deep inside the method rather than at the call site where the nil was introduced. Stack traces help, but the root cause can be several frames away from the crash line.
Why does my struct not satisfy an interface when I implement all the methods?
The most common reason is a pointer receiver mismatch. If any method in the interface is implemented with a pointer receiver on your struct, only *YourStruct satisfies the interface — not YourStruct. Check whether you’re passing a value or a pointer to the function or assignment that expects the interface. The second most common reason is a typo in the method signature — a parameter type or return type that doesn’t exactly match the interface definition. The error message does not implement (wrong type for method X) tells you which method is the problem; from there, compare your implementation signature against the interface definition character by character.
Golang Receivers: The Silent Architect of Your Technical Debt
Look, Ive been there: you write a method, the logic is solid, the tests are green, but in production, your data just “evaporates.” This is the classic golang receiver mistake that trips up even seniors moving from Java or Python. In those languages, you’re used to objects being references by default. Golang is raw and honest: if you forget that “star,” you aren’t working with your data—youre working with a ghost.
The worst part? The compiler stays dead silent. It won’t tap you on the shoulder and say, “Hey bro, you just copied a mutex and your service is going to choke under pressure.” Youre creating a full copy of the struct in memory, mutating it, and immediately tossing it into the trash. Its not just a bug; its an architectural sinkhole that makes refactoring a nightmare when interfaces suddenly stop “recognizing” your types.
My expert verdict: You either train yourself to feel the difference between a pointer vs value receiver immediately, or golang will teach you the hard way through sleepless nights of debugging. If your struct has even one pointer or a mutex—forget value receivers exist. In this language, explicit is always better than implicit. Understanding exactly where your data sits in memory is the thin line between being a “coder” and a real Gopher.
Written by: