Question
In Go, when should I use a value receiver instead of always using a pointer receiver?
For reference, consider this type and its methods:
type T struct {
a int
}
func (tv T) Mv(a int) int { return 0 } // value receiver
func (tp *T) Mp(f float32) float32 { return 1 } // pointer receiver
The Go documentation says that for basic types, slices, and small structs, a value receiver is often very cheap, so unless the method's semantics require a pointer, a value receiver can be efficient and clear.
That raises a few questions:
- Is a value receiver actually cheaper than a pointer receiver, or just cheap enough that the difference often does not matter?
- In benchmarks, pointer receivers can appear faster even for very small structs. For example, with a struct containing a single field, the pointer receiver may outperform the value receiver.
- The documentation also says value receivers can be "clear." Is that mainly about readability and semantics rather than raw performance?
- If consistency matters, why not always use pointer receivers?
I would like to understand real cases where a value receiver clearly makes more sense than a pointer receiver, and whether microbenchmarks might be missing other important trade-offs.
Short Answer
By the end of this page, you will understand the real difference between value receivers and pointer receivers in Go, why this choice is mostly about method semantics and method sets rather than tiny benchmark differences, and how to choose the right receiver type in real code.
Concept
In Go, a method receiver decides how a method gets access to a value.
A method can receive either:
- a copy of the value using a value receiver
- a pointer to the value using a pointer receiver
Example:
type Counter struct {
n int
}
func (c Counter) Value() int {
return c.n
}
func (c *Counter) Increment() {
c.n++
}
Here is the key idea:
Value()gets its own copy ofCounterIncrement()gets access to the originalCounterthrough a pointer
Why this matters
The choice is not only about speed. It affects:
- Whether the method can modify the original value
- Whether copies are made
- Which interfaces the type implements
- How clearly the API communicates intent
- Whether copying the value is safe or desirable
Value receiver meaning
Mental Model
Think of a value receiver and a pointer receiver like two ways of handing someone a document.
- Value receiver: you hand them a photocopy
- Pointer receiver: you hand them the original document
If they write on the photocopy, your original stays unchanged. If they write on the original, everyone sees the change.
In Go:
- a value receiver works on its own copy
- a pointer receiver works on the original value through its address
This helps answer the main design question:
- If the method should only inspect data, a copy may be fine.
- If the method should update shared state, it needs the original.
For small structs, making a photocopy is cheap. For large structs or sensitive structs, you may not want copies at all.
Syntax and Examples
In Go, both receiver styles look similar, but they behave differently.
Basic syntax
type User struct {
Name string
Age int
}
// Value receiver
func (u User) IsAdult() bool {
return u.Age >= 18
}
// Pointer receiver
func (u *User) HaveBirthday() {
u.Age++
}
Example: value receiver does not change the original
package main
import "fmt"
type Counter struct {
n int
}
func (c Counter) AddOneWrong() {
c.n++
}
func main() {
c := Counter{n: 5}
c.AddOneWrong()
fmt.Println(c.n) // 5
}
Why?
AddOneWrong() receives a copy of c, so incrementing c.n changes only the copy.
Step by Step Execution
Consider this example:
package main
import "fmt"
type Score struct {
points int
}
func (s Score) TryAdd() {
s.points += 10
fmt.Println("inside TryAdd:", s.points)
}
func (s *Score) Add() {
s.points += 10
fmt.Println("inside Add:", s.points)
}
func main() {
score := Score{points: 20}
score.TryAdd()
fmt.Println("after TryAdd:", score.points)
score.Add()
fmt.Println("after Add:", score.points)
}
Execution trace
1. Create the value
score := Score{points: 20}
Now score.points is 20.
2. Call the value receiver method
score.TryAdd()
Because TryAdd has a value receiver, Go passes a of into the method.
Real World Use Cases
When value receivers make sense
Small value objects
Types that are naturally treated as plain values often use value receivers:
- coordinates
- RGB colors
- money amounts
- timestamps or durations
- simple geometry types
Example:
type Point struct {
X, Y int
}
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
This does not need shared mutable state.
Computed helper methods
Methods that only calculate or format something are often good candidates.
type FileInfo struct {
Name string
Size int64
}
func (f FileInfo) IsEmpty() bool {
return f.Size == 0
}
Safer APIs for immutable-style behavior
A value receiver can communicate: "this method does not change the receiver."
That can make code easier to reason about.
When pointer receivers make sense
Mutating methods
Real Codebase Usage
In real Go codebases, developers often choose receiver types based on a few practical patterns.
1. Read-only methods on small value types
For tiny domain types, value receivers are common.
type Temperature struct {
Celsius float64
}
func (t Temperature) Fahrenheit() float64 {
return t.Celsius*9/5 + 32
}
This type behaves like a number with helper methods.
2. Mutating methods use pointer receivers
This is one of the clearest patterns.
type Config struct {
Debug bool
}
func (c *Config) EnableDebug() {
c.Debug = true
}
3. If one method needs a pointer, many teams use pointers for all methods
This avoids a mixed method set unless there is a strong reason not to.
type Buffer struct {
data []byte
}
func (b *Buffer) Write(p []byte) {
b.data = (b.data, p...)
}
Len() {
(b.data)
}
Common Mistakes
1. Using a value receiver when you expect mutation
Broken example:
type Counter struct {
n int
}
func (c Counter) Increment() {
c.n++
}
This does not change the original value.
Fix:
func (c *Counter) Increment() {
c.n++
}
2. Assuming a value receiver makes everything immutable
Broken assumption:
type Data struct {
Items []int
}
func (d Data) Change() {
d.Items[0] = 99
}
Even though d is a copy, the slice inside it still points to shared backing data.
To avoid surprises:
- remember that slices, maps, channels, and function values are reference-like
- copy nested data explicitly when needed
3. Mixing receiver types carelessly
Go allows mixing value and pointer receivers, but it can make APIs confusing.
Comparisons
| Aspect | Value Receiver | Pointer Receiver |
|---|---|---|
| Receives | A copy of the value | A pointer to the original value |
| Can modify original? | No | Yes |
| Copy cost | May copy the struct | Avoids copying the struct itself |
| Good for small value-like types? | Yes | Sometimes, but not always needed |
| Good for large structs? | Usually less ideal | Usually better |
| Interface method set | Method belongs to both T and *T | Method belongs only to *T |
| Works with mutex-containing structs? | Dangerous if copied | Preferred |
Cheat Sheet
Quick rules
- Use a pointer receiver if the method must modify the receiver.
- Use a pointer receiver if the struct is large.
- Use a pointer receiver if the struct contains
sync.Mutex,sync.RWMutex, or similar state. - Use a value receiver for small, value-like, read-only types.
- If you mix receiver types, do it intentionally.
Syntax
func (v T) Method() {}
func (p *T) Method() {}
Key behavior
type C struct{ n int }
func (c C) A() { c.n++ } // changes copy only
func (c *C) B() { c.n++ } // changes original
Method set rule
If:
func (t T) V() {}
func P() {}
FAQ
When should I use a value receiver in Go?
Use a value receiver when the method does not need to modify the receiver and the type is small, simple, and naturally value-like.
Are pointer receivers always faster in Go?
No. They can avoid copying, but the real performance difference depends on the type, compiler optimizations, and usage pattern. Tiny benchmarks often do not reflect real application performance.
Can a value receiver method change slice or map contents?
Yes. The struct itself is copied, but slices and maps refer to underlying shared data. Changing that underlying data can still affect the original.
Why do pointer receivers affect interface implementation?
Because methods with receiver *T belong only to the method set of *T, not T. That means only pointer values implement an interface requiring those methods.
Should I use pointer receivers for all methods on a type?
Often yes if the type is mutable, large, or already has mutating methods. But for small value-like types, value receivers can better express intent.
Is mixing value and pointer receivers allowed?
Yes, but do it carefully. Mixing can make code harder to understand, especially when interfaces are involved.
Why is copying a struct with a mutex dangerous?
A mutex has internal state and should not be copied after use. Using value receivers on such structs can accidentally copy the mutex and lead to incorrect behavior.
What matters more: semantics or benchmark speed?
Usually semantics. Choose the receiver type that matches the meaning of the method and the safety of the type. Optimize later if profiling shows a real issue.
Mini Project
Description
Build a small Go program that models a bank account and a transaction summary. This project demonstrates when value receivers are a good fit for read-only behavior and when pointer receivers are required for mutation.
Goal
Create a program where read-only methods use value receivers and state-changing methods use pointer receivers.
Requirements
- Create an
Accountstruct with owner name and balance. - Add a value receiver method that returns a formatted account summary.
- Add a pointer receiver method for depositing money.
- Add a pointer receiver method for withdrawing money if enough balance exists.
- Show in
mainthat summary methods do not change state, but deposit and withdraw do.
Keep learning
Related questions
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Calling Functions Across Files in the Same Go Package
Learn how Go uses packages across multiple files, why functions may appear undefined, and how to organize code correctly.