Method Receivers in Go: Demystifying Value and Pointer Receivers
Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy. Go doesn't have traditional classes or inheritance, but it does let you associate methods with types. This happens through method receivers. If you've ever wondered whether to use a value or a pointer receiver in Go, this is the detailed, no-nonsense guide for you. What Are Method Receivers in Go? A method receiver in Go is the value or pointer on which a method operates. It is defined like so: func (r ReceiverType) MethodName(...) ReturnType { // implementation } This makes MethodName a method tied to ReceiverType. Go doesn't have classes, but this is how you associate behavior with data. Unlike Java or C++, Go makes the receiver explicit, and it can either be a value or a pointer. Two Kinds of Receivers: Value and Pointer Go supports two types of method receivers: Receiver Type Syntax Works On Allows Mutation Allocates on Heap Value (t T) Copy No No Pointer (t *T) Original Yes Maybe Value Receivers: Call by Value Syntax func (u User) Greet() string { return "Hello, " + u.name } Behavior Copies the struct when the method is called. Cannot modify the original struct. Preferred for small, immutable-like types. Example type User struct { name string } func (u User) Rename(newName string) { u.name = newName // modifies copy } func main() { user := User{name: "Alice"} user.Rename("Bob") fmt.Println(user.name) // still "Alice" } When to Use The struct is small. You don't need to modify fields. You want consistent behavior (immutability-style). More details: Effective Go: Methods Pointer Receivers: Reference-Like Semantics Syntax func (u *User) Rename(newName string) { u.name = newName } Behavior Operates on the original struct. Can modify the receiver's internal state. Can be more efficient for large structs. Example type User struct { name string } func (u *User) Rename(newName string) { u.name = newName } func main() { user := User{name: "Alice"} user.Rename("Bob") fmt.Println(user.name) // "Bob" } When to Use You want to modify the receiver. The struct is large or contains reference-heavy fields. You already use pointer receivers elsewhere on the type. More details: Go FAQ: Pointers Method Sets and Their Role Go defines what methods a type has through method sets: For T: includes all methods with value receivers. For *T: includes all methods with pointer or value receivers. This impacts whether a type can satisfy an interface. Example type Stringer interface { String() string } type Data struct { value int } func (d *Data) String() string { return fmt.Sprintf("%d", d.value) } func main() { var s Stringer d := Data{42} // s = d // Compile error: Data does not implement Stringer s = &d // OK: *Data has String method fmt.Println(s.String()) } How Go Handles Calls: Implicit Conversions Go is flexible when it comes to calling methods: Method Defined On Can Be Called With T T or *T *T *T only type Counter struct { count int } func (c Counter) Print() {...} // Can call with value or pointer func (c *Counter) Increment() {...} // Only with pointer When calling value.Increment(), Go will auto-take the address if needed — but only if the method has a pointer receiver. Real Performance Implications Value Receiver type Big struct { data [1024]int } func (b Big) Process() { // copies 8KB } Pointer Receiver type Big struct { data [1024]int } func (b *Big) Process() { // avoids copy } Using a value receiver here would trigger a full copy on each method call. Prefer pointer receivers when working with large data types or when mutation is required. Read more: Go Memory Model Consistency Rule: Avoid Mixing Receiver Types If one method on a type uses a pointer receiver, all methods should. This avoids: Confusion about behavior. Interface method mismatch. Implicit address/dereference bugs. type Confused struct { name string } func (c Confused) GetName() string { return c.name } func (c *Confused) SetName(n string) { c.name = n } Better: type Clear struct { name string } func (c *Clear) GetName() string { return c.name } func (c *Clear) SetName(n string) { c.name = n } Receiver Behavior with Maps, Slices, Channels Value receivers do not protect against mutation if fields themselves are reference types: type Data struct { values []int } func (d Data) Add(val int) { d.values = append(d.values, val) // modifies shared slice }

Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a tool that makes generating API docs from your code ridiculously easy.
Go doesn't have traditional classes or inheritance, but it does let you associate methods with types.
This happens through method receivers.
If you've ever wondered whether to use a value or a pointer receiver in Go, this is the detailed, no-nonsense guide for you.
What Are Method Receivers in Go?
A method receiver in Go is the value or pointer on which a method operates. It is defined like so:
func (r ReceiverType) MethodName(...) ReturnType {
// implementation
}
This makes MethodName
a method tied to ReceiverType
. Go doesn't have classes, but this is how you associate behavior with data.
Unlike Java or C++, Go makes the receiver explicit, and it can either be a value or a pointer.
Two Kinds of Receivers: Value and Pointer
Go supports two types of method receivers:
Receiver Type | Syntax | Works On | Allows Mutation | Allocates on Heap |
---|---|---|---|---|
Value | (t T) |
Copy | No | No |
Pointer | (t *T) |
Original | Yes | Maybe |
Value Receivers: Call by Value
Syntax
func (u User) Greet() string {
return "Hello, " + u.name
}
Behavior
- Copies the struct when the method is called.
- Cannot modify the original struct.
- Preferred for small, immutable-like types.
Example
type User struct {
name string
}
func (u User) Rename(newName string) {
u.name = newName // modifies copy
}
func main() {
user := User{name: "Alice"}
user.Rename("Bob")
fmt.Println(user.name) // still "Alice"
}
When to Use
- The struct is small.
- You don't need to modify fields.
- You want consistent behavior (immutability-style).
More details: Effective Go: Methods
Pointer Receivers: Reference-Like Semantics
Syntax
func (u *User) Rename(newName string) {
u.name = newName
}
Behavior
- Operates on the original struct.
- Can modify the receiver's internal state.
- Can be more efficient for large structs.
Example
type User struct {
name string
}
func (u *User) Rename(newName string) {
u.name = newName
}
func main() {
user := User{name: "Alice"}
user.Rename("Bob")
fmt.Println(user.name) // "Bob"
}
When to Use
- You want to modify the receiver.
- The struct is large or contains reference-heavy fields.
- You already use pointer receivers elsewhere on the type.
More details: Go FAQ: Pointers
Method Sets and Their Role
Go defines what methods a type has through method sets:
- For
T
: includes all methods with value receivers. - For
*T
: includes all methods with pointer or value receivers.
This impacts whether a type can satisfy an interface.
Example
type Stringer interface {
String() string
}
type Data struct {
value int
}
func (d *Data) String() string {
return fmt.Sprintf("%d", d.value)
}
func main() {
var s Stringer
d := Data{42}
// s = d // Compile error: Data does not implement Stringer
s = &d // OK: *Data has String method
fmt.Println(s.String())
}
How Go Handles Calls: Implicit Conversions
Go is flexible when it comes to calling methods:
Method Defined On | Can Be Called With |
---|---|
T |
T or *T
|
*T |
*T only |
type Counter struct {
count int
}
func (c Counter) Print() {...} // Can call with value or pointer
func (c *Counter) Increment() {...} // Only with pointer
When calling value.Increment()
, Go will auto-take the address if needed — but only if the method has a pointer receiver.
Real Performance Implications
Value Receiver
type Big struct {
data [1024]int
}
func (b Big) Process() {
// copies 8KB
}
Pointer Receiver
type Big struct {
data [1024]int
}
func (b *Big) Process() {
// avoids copy
}
Using a value receiver here would trigger a full copy on each method call. Prefer pointer receivers when working with large data types or when mutation is required.
Read more: Go Memory Model
Consistency Rule: Avoid Mixing Receiver Types
If one method on a type uses a pointer receiver, all methods should. This avoids:
- Confusion about behavior.
- Interface method mismatch.
- Implicit address/dereference bugs.
type Confused struct {
name string
}
func (c Confused) GetName() string {
return c.name
}
func (c *Confused) SetName(n string) {
c.name = n
}
Better:
type Clear struct {
name string
}
func (c *Clear) GetName() string {
return c.name
}
func (c *Clear) SetName(n string) {
c.name = n
}
Receiver Behavior with Maps, Slices, Channels
Value receivers do not protect against mutation if fields themselves are reference types:
type Data struct {
values []int
}
func (d Data) Add(val int) {
d.values = append(d.values, val) // modifies shared slice
}
Even though d
is a value receiver, the underlying slice may be shared and mutated.
Interface Compatibility and Traps
Scenario
You define an interface that expects a method with pointer receiver:
type Writer interface {
WriteData() error
}
type File struct {}
func (f *File) WriteData() error {...}
var w Writer
var f File
w = &f // valid
w = f // invalid: File does not implement Writer
You must use the pointer if the method is defined on *T
.
Summary: Choosing the Right Receiver
Criteria | Value Receiver | Pointer Receiver |
---|---|---|
Needs to modify receiver? | ❌ No | ✅ Yes |
Struct is large? | ❌ Avoid | ✅ Prefer |
All methods should match? | ❌ Risky | ✅ Safer |
Avoid heap allocation? | ✅ Usually | ❌ Maybe escapes |
Rule of Thumb
- If any method needs a pointer receiver, make all methods use pointer receivers.
- Keep behavior and memory usage predictable.
For More Reading
Got questions or deeper edge cases? Leave a comment. This topic is subtle but critical to mastering Go.