Hello! Welcome to the next lesson in your Go journey.
In our last session, we saw how to implement methods on structs, bundling data and behavior together. We introduced the fundamental distinction between value receivers (func (t T)) for read-only operations and pointer receivers (func (t *T)) for modifications.
Today, we will build on that foundation to fully answer the question: when should you use a value receiver, and when should you use a pointer receiver? The choice isn't always as simple as "read vs. write." This lesson will give you a robust mental model for making this decision, focusing on correctness, efficiency, and idiomatic Go conventions. Mastering this is key to writing clean and effective Go code, especially for the kind of robust automation tools you aim to build.
The Three Rules for Choosing a Receiver
There are three main considerations when choosing between a value and a pointer receiver. We'll go through them in order of importance.
1. The Rule of Modification (A "Must")
This is the most critical rule. If a method needs to modify the state of the receiver struct, it must use a pointer receiver.
As we saw briefly in the last lesson, a value receiver operates on a copy of the struct. Any changes are made to that copy and are discarded when the method returns. A pointer receiver, however, gets the memory address of the original struct, allowing it to make persistent changes.
Let's watch a practical demonstration that makes this concept crystal clear.
Go Pointers: When & How To Use Them Efficiently
The video 'Go Pointers: When & How To Use Them Efficiently' by Anthony GG provides a perfect example of this rule. It shows how an updateEmail method fails with a value receiver but succeeds with a pointer receiver.
Watch the section from 03:19 to 06:33. Pay close attention to the updateEmail function. Notice how the email fails to update with the value receiver (u User) and how changing it to a pointer receiver (u *User) fixes the issue.
This is the non-negotiable reason for using a pointer. If you need to change the struct, you have no other choice. A common use case in DevOps tooling is a method that updates the status of a resource, like a server or a deployment.
type Server struct {
Hostname string
Status string // e.g., "running", "stopped"
}
// Stop changes the server's status. It MUST use a pointer receiver.
func (s *Server) Stop() {
s.Status = "stopped"
}
2. The Rule of Efficiency (A "Should")
What if your method doesn't modify the struct? The decision becomes less obvious. The next factor to consider is performance.
Every time you call a method with a value receiver, Go copies the entire struct.
- For small structs (like our
Serverexample with two strings), this copy is incredibly cheap and not a concern. - For large structs, however, this can be inefficient. Imagine a struct representing a VM configuration that contains a large block of user data or a log history:
type VMConfig struct {
ID string
Name string
CPUCores int
MemoryMB int
UserData []byte // This could be large, e.g., a multi-KB cloud-init script
}
// GetID reads a field. It doesn't modify the struct.
func (c VMConfig) GetID() string { // This copies the entire struct, including UserData!
return c.ID
}
Calling GetID() would create a complete copy of the VMConfig instance, including the potentially large UserData slice. If this method is called frequently in a loop, these repeated allocations can impact performance.
By using a pointer receiver, you only copy a memory address (typically 8 bytes on a 64-bit system), which is much more efficient.
// GetID with a pointer receiver is more efficient for a large struct.
func (c *VMConfig) GetID() string { // This only copies an 8-byte pointer.
return c.ID
}
The same video has a great segment explaining this.
Go Pointers: When & How To Use Them Efficiently
Let's continue with the same video, which now explains the performance implications of copying large objects.
Watch from 07:35 to 10:51. The presenter discusses how a struct with a large field (like a file buffer) can be expensive to copy and why using a pointer is preferable in such cases.
Guideline: If your struct is large or could become large, use a pointer receiver for all its methods, even read-only ones, to avoid performance penalties from copying.
3. The Rule of Consistency (An "Ought To")
Let's say your method doesn't modify the struct, and the struct is small. Should you use a value receiver? Not so fast. The final rule is about consistency and predictability.
The idiomatic convention in Go is: If any method on a type has a pointer receiver, then all methods on that type should have a pointer receiver.
There are two reasons for this:
- Clarity for the User: It makes the type's behavior predictable. The user doesn't have to guess whether they are working with a value or a pointer.
- Method Sets and Interfaces: This is the crucial technical reason. A type's "method set" determines which interfaces it satisfies.
- The method set of a value type
Tincludes only methods with a value receiver(t T). - The method set of a pointer type
*Tincludes methods with both value(t T)and pointer(t *T)receivers.
- The method set of a value type
This image provides a great visual explanation of how Go handles method calls on values versus pointers.

Let's see why this matters. Imagine an interface for tools that can be started and stopped:
type Runnable interface {
Start()
Status() string
}
And a Tool struct with mixed receivers:
type Tool struct {
name string
status string
}
func (t *Tool) Start() { // Pointer receiver (modifies status)
t.status = "running"
}
func (t Tool) Status() string { // Value receiver (reads status)
return t.status
}
Now, let's try to use it:
func main() {
myTool := Tool{name: "backup-agent"}
var r Runnable
r = &myTool // This works! *Tool has both Start() and Status().
// r = myTool // This would fail! Tool (the value type) only has Status() in its method set.
// It does not have Start(), so it doesn't satisfy the Runnable interface.
}
By mixing receivers, you create a situation where *Tool satisfies the interface, but Tool does not. This is confusing and error-prone. If you make all receivers pointers, this ambiguity disappears.
The following article provides a clear summary table and explains this consistency rule well.
Method Receivers in Go: Demystifying Value and Pointer ...
The article 'Method Receivers in Go' provides a great recap and dives into the consistency rule. The summary table is particularly helpful.
Read the sections 'Consistency Rule: Avoid Mixing Receiver Types' and 'Summary: Choosing the Right Receiver'. Focus on the reasons for consistency and study the final summary table.
A Simple Decision Guide
To summarize, here's a simple process for choosing a receiver type:
- Does the method need to modify the receiver?
- Yes -> Use a pointer receiver. End of story.
- Does the method not modify the receiver?
- Is the struct large or contains fields that might grow (e.g., slices, maps)?
- Yes -> Use a pointer receiver for efficiency.
- Is the struct small and all other methods on this type use value receivers?
- Yes -> A value receiver is acceptable.
- Do any other methods on this type use a pointer receiver?
- Yes -> Use a pointer receiver for consistency.
- Is the struct large or contains fields that might grow (e.g., slices, maps)?
Given your goal of building robust tools, the consistency rule is your best friend. When in doubt, a pointer receiver is often the safer, more flexible choice.
Test your understanding!
You are building a configuration manager for a CLI tool. You have the following struct:
type AppConfig struct {
Profile string
LogLevel string
TimeoutSecs int
}
You need to implement these three methods:
SetLogLevel(level string): Changes theLogLevelfield.ProfileName() string: Returns theProfilename.Validate() error: Checks if the configuration is valid (e.g.,TimeoutSecs > 0). This is a read-only check.
For each method, decide whether it should have a value or pointer receiver and explain your reasoning based on the three rules.
Show answer
Based on the rules, here are the idiomatic signatures:
-
func (c *AppConfig) SetLogLevel(level string)- Reason: It must use a pointer receiver because it modifies the
LogLevelfield (Rule 1).
- Reason: It must use a pointer receiver because it modifies the
-
func (c *AppConfig) ProfileName() string- Reason: Even though this is a read-only operation and
AppConfigis small, we should use a pointer receiver for consistency (Rule 3), sinceSetLogLevelrequires one.
- Reason: Even though this is a read-only operation and
-
func (c *AppConfig) Validate() error- Reason: Same as above. We use a pointer receiver for consistency (Rule 3) with
SetLogLevel.
- Reason: Same as above. We use a pointer receiver for consistency (Rule 3) with
By making all three use *AppConfig, the AppConfig type presents a consistent and predictable interface to its users.
An Important Nuance: Nil Receivers
A final practical point: methods with pointer receivers can be called on nil pointers without causing a panic. The receiver inside the method will be nil, and you can (and should) check for this.
type Reporter struct {
URL string
}
func (r *Reporter) Report(message string) {
if r == nil {
fmt.Println("Reporting skipped: reporter not configured.")
return
}
fmt.Printf("Reporting '%s' to %s\n", message, r.URL)
}
func main() {
var r *Reporter // r is nil
r.Report("system is healthy") // This is safe!
// Output: Reporting skipped: reporter not configured.
}
This pattern is useful for providing safe default behavior when working with optional components, a common scenario in configurable tools.
Conclusion
You now have a comprehensive framework for deciding between value and pointer receivers. While the initial "read vs. write" distinction is a good start, true Go proficiency comes from applying these more nuanced rules.
Key Takeaways:
- Modification is mandatory: If you change state, you must use a pointer receiver.
- Efficiency is important: For large structs, you should use pointer receivers to avoid costly copies.
- Consistency is idiomatic: If any method needs a pointer, all methods ought to use pointers for clarity and predictable interface satisfaction.
- When in doubt, especially for the types that model the core entities of your application, defaulting to a pointer receiver is a safe and idiomatic bet.
Next Up
This lesson focused on pointers in the specific context of method receivers. In our next lesson, we will broaden our scope to understand and use pointers to manage memory and share data effectively in other contexts. This will complete your foundational knowledge of how Go manages data in memory.