In Go, deciding whether to use pointers or values for method receivers and function parameters is a fundamental choice that impacts performance, safety, and code behavior. While the language provides clear guidelines, the decision often depends on context—such as framework requirements or data mutability needs.
In this post, we’ll explore the tradeoffs between pointers and values in Go, with practical examples and guidelines to help you make informed decisions in your code.
Understanding Pointers and Values
Before diving into tradeoffs, let’s clarify what pointers and values mean in Go:
- Values: Direct copies of data. When you pass a struct as a value, Go creates a complete copy.
- Pointers: References to data in memory. Passing a pointer shares the original data.
type Person struct {
Name string
Age int
}
func (p Person) Birthday() { // Value receiver
p.Age++ // Only modifies the copy
}
func (p *Person) Birthday() { // Pointer receiver
p.Age++ // Modifies the original
}
Tradeoffs of Using Pointers vs. Values
Advantages of Pointers
- Modification of the receiver: Pointer receivers allow methods to modify the original struct’s fields. Value receivers work on a copy, so changes are lost.
- Efficiency for large structs: Passing a pointer avoids copying the entire struct (which can be expensive for large data). This is especially relevant for structs with many fields or embedded data.
- Interface compliance: Some interfaces require pointer receivers to ensure the method can access or modify the underlying data without copying.
- Nil safety in some cases: Pointers can represent “absence” (nil), which is useful for optional or uninitialized structs.
- Consistency: If any method on a type uses a pointer receiver (e.g., to modify state), all methods should use pointers to avoid confusion and ensure the type behaves uniformly.
Disadvantages of Pointers
- Nil panics: Dereferencing a nil pointer causes a runtime panic, which can crash the program if not handled (e.g., via checks or initialization).
- Slight overhead: Pointers add a small memory and indirection cost compared to direct values.
- Concurrency risks: Shared pointers can lead to race conditions in concurrent code if not protected (e.g., with mutexes), as multiple goroutines might modify the same data.
- Less safe for small structs: For tiny structs (e.g., with just a few int fields), copying is often faster and safer than indirection.
- Immutability loss: Values promote immutability by default, while pointers allow accidental mutation.
Advantages of Values
- Safety and simplicity: No nil panics, and methods can’t accidentally modify the original data (promotes immutability).
- Performance for small data: Copying small structs (e.g., primitives or simple aggregates) is often cheaper than pointer indirection.
- Thread safety: Values are inherently safe in concurrent contexts since each call gets a copy.
Disadvantages of Values
- No modification: Methods can’t change the original struct; they must return updated copies.
- Inefficiency for large data: Copying large structs wastes memory and CPU, especially in loops or recursive calls.
- Interface limitations: Some APIs require pointers to allow state changes across calls.
Practical Examples
Let’s look at some concrete examples to illustrate these tradeoffs:
// Small struct - values are often better
type Point struct {
X, Y int
}
func (p Point) Distance() float64 {
// Value receiver: safe, no side effects
return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}
// Large struct - pointers for efficiency
type UserProfile struct {
ID int
Name string
Email string
// ... many more fields
Settings map[string]interface{}
}
func (u *UserProfile) UpdateEmail(email string) {
// Pointer receiver: modifies original, efficient for large struct
u.Email = email
}
// Interface requiring pointers
type Writer interface {
Write([]byte) (int, error)
}
type FileWriter struct {
file *os.File
}
func (fw *FileWriter) Write(data []byte) (int, error) {
// Must be pointer to modify internal state
return fw.file.Write(data)
}
General Guidelines for Using Pointers vs. Values
Use pointers when:
- The method needs to modify the receiver’s fields (e.g., updating state in a model).
- The struct is large (rule of thumb: >1-2 words in size, or contains slices/maps/channels).
- Required by an interface or framework.
- The struct might be nil or optional (e.g., error handling).
- For consistency: If any method on the type uses a pointer, use pointers for all methods to avoid mixing semantics.
Use values when:
- The struct is small and immutable (e.g., coordinates, simple configs).
- Methods don’t need to modify the receiver (e.g., getters like
Title()
orDescription()
). - You want to ensure thread safety without locks (each call gets a safe copy).
- The type is a primitive or basic aggregate (e.g.,
time.Time
often uses values for simplicity).
Receiver rules (from Effective Go):
- Value receivers: Can be called on both values and pointers (Go auto-dereferences).
- Pointer receivers: Can only be called on pointers (Go auto-takes address for addressable values, like variables).
- If in doubt, profile performance—pointers aren’t always faster (e.g., small structs copy quickly).
Conclusion
Choosing between pointers and values in Go is about balancing performance, safety, and code clarity. Pointers excel when you need mutability or efficiency with large data, but they introduce risks like nil panics and concurrency issues. Values provide safety and simplicity but can be inefficient for large structures.
The key is to follow Go’s conventions and consider your specific use case. When in doubt, start with values for their safety guarantees, and only switch to pointers when profiling shows a clear performance benefit or when required by interfaces.
For more in-depth guidance, I recommend reading the “Pointers vs. Values” section in Effective Go. Remember, good Go code is often about making these decisions consistently and thoughtfully throughout your codebase.