A method in Go is a function with a receiver — an extra parameter that appears before the function name. The receiver specifies which type the method belongs to. Methods let you attach behavior to any named type, not just structs, and they’re how Go implements the behavior side of its interface system.
The choice between a value receiver and a pointer receiver has real consequences for correctness, performance, and interface satisfaction. It’s not just a style choice.
For struct definition and embedding see Go structs composition and embedding. For interfaces see Go interfaces.
Value Receivers
A value receiver receives a copy of the value. The method operates on that copy — any changes to the receiver inside the method don’t affect the original:
type Circle struct {
Radius float64
}
// Value receiver: Area gets a copy of c
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// String satisfies fmt.Stringer — conventionally a value receiver
func (c Circle) String() string {
return fmt.Sprintf("Circle(r=%.2f)", c.Radius)
}
Value receivers are appropriate for:
- Small structs where copying is cheap (a few fields of basic types)
- Read-only operations that don’t need to modify the receiver
- Types that should behave like values — structs that are semantically equivalent to their field values, like
time.Timeornet.IP
Pointer Receivers
A pointer receiver receives a pointer to the value. The method can modify the original:
type Stack struct {
items []int
}
// Pointer receiver: Push modifies the original Stack
func (s *Stack) Push(item int) {
s.items = append(s.items, item)
}
func (s *Stack) Pop() (int, bool) {
if len(s.items) == 0 {
return 0, false
}
n := len(s.items)
item := s.items[n-1]
s.items = s.items[:n-1]
return item, true
}
func (s *Stack) Len() int {
return len(s.items)
}
Pointer receivers are appropriate for:
- Mutations — any method that changes the receiver’s state
- Large structs where copying would be expensive
- Types with mutable state — anything with internal counters, buffers, or connections
The Consistency Rule
If any method on a type uses a pointer receiver, all methods should use pointer receivers. Mixing them on the same type creates confusion about method sets and interface satisfaction:
// ❌ Inconsistent — some value, some pointer
type User struct{ Name string }
func (u User) GetName() string { return u.Name } // value
func (u *User) SetName(s string) { u.Name = s } // pointer
func (u User) String() string { return u.Name } // value
// ✅ Consistent — all pointer
type User struct{ Name string }
func (u *User) GetName() string { return u.Name }
func (u *User) SetName(s string) { u.Name = s }
func (u *User) String() string { return u.Name }
The exception is String() string for fmt.Stringer and Error() string for error — these are conventionally value receivers because you usually want both User and *User to print nicely. But if all other methods use pointer receivers, use pointer for String() too.
Method Sets and Interface Satisfaction
This is where value vs pointer receivers has real impact. Go’s method sets:
- A value of type
Thas the method set of all methods with value receiver(t T) - A pointer of type
*Thas the method set of all methods with either value or pointer receiver
In plain English: a pointer can call both kinds of methods, but a value can only call value-receiver methods.
This matters for interface satisfaction:
type Writer interface {
Write(data string) error
}
type FileWriter struct{ path string }
func (fw *FileWriter) Write(data string) error {
// ... write to file ...
return nil
}
var w Writer = &FileWriter{path: "out.txt"} // ✅ *FileWriter satisfies Writer
var w Writer = FileWriter{path: "out.txt"} // ❌ compile error: FileWriter does not implement Writer
FileWriter (value) doesn’t satisfy Writer because Write has a pointer receiver. Only *FileWriter satisfies it. The fix is almost always to use &FileWriter{} — and that’s fine, you should be using pointers for mutable types anyway.
A compile-time assertion makes this contract explicit:
var _ Writer = (*FileWriter)(nil) // fails to compile if *FileWriter doesn't implement Writer
Go’s Automatic Address-Taking
When you call a pointer-receiver method on an addressable value, Go automatically takes its address:
s := Stack{} // value, not pointer
s.Push(42) // Go rewrites this as (&s).Push(42) — works fine
This convenience only works for addressable values (variables, struct fields, array elements). It doesn’t work for non-addressable values like function return values or map elements:
// ❌ Cannot take address of map[string]Stack{}["key"]
m := map[string]Stack{}
m["key"].Push(42) // compile error
// ✅ Use a pointer in the map
m := map[string]*Stack{}
m["key"] = &Stack{}
m["key"].Push(42)
Method Chaining
Returning the receiver from a pointer-receiver method enables fluent chaining:
type QueryBuilder struct {
table string
wheres []string
limit int
offset int
}
func (q *QueryBuilder) From(table string) *QueryBuilder {
q.table = table
return q
}
func (q *QueryBuilder) Where(condition string) *QueryBuilder {
q.wheres = append(q.wheres, condition)
return q
}
func (q *QueryBuilder) Limit(n int) *QueryBuilder {
q.limit = n
return q
}
func (q *QueryBuilder) Build() string {
sql := fmt.Sprintf("SELECT * FROM %s", q.table)
if len(q.wheres) > 0 {
sql += " WHERE " + strings.Join(q.wheres, " AND ")
}
if q.limit > 0 {
sql += fmt.Sprintf(" LIMIT %d", q.limit)
}
return sql
}
// Usage
query := (&QueryBuilder{}).
From("users").
Where("age > 18").
Where("active = true").
Limit(100).
Build()
// SELECT * FROM users WHERE age > 18 AND active = true LIMIT 100
The chaining pattern works because each method returns *QueryBuilder, allowing the next call immediately on the result.
Methods on Non-Struct Types
Methods work on any named type, not just structs. This is useful for adding behavior to primitive-based types:
type Duration int64 // represents nanoseconds
func (d Duration) Seconds() float64 { return float64(d) / 1e9 }
func (d Duration) String() string { return fmt.Sprintf("%.2fs", d.Seconds()) }
type Celsius float64
func (c Celsius) ToFahrenheit() float64 { return float64(c)*9/5 + 32 }
func (c Celsius) String() string { return fmt.Sprintf("%.1f°C", c) }
And on slice types for sorting and filtering:
type UserList []User
func (ul UserList) Len() int { return len(ul) }
func (ul UserList) Less(i, j int) bool { return ul[i].Name < ul[j].Name }
func (ul UserList) Swap(i, j int) { ul[i], ul[j] = ul[j], ul[i] }
// UserList now satisfies sort.Interface
sort.Sort(UserList(users))
Summary
- Value receiver: gets a copy; use for reads, small immutable types,
String()andError()by convention - Pointer receiver: gets a pointer; required for mutations, large structs, any type with internal state
- The consistency rule: if any method uses
*T, use*Tfor all methods on that type - Only
*Tsatisfies an interface when the implementing method has a pointer receiver —Tdoes not - Go auto-takes address for addressable values, but not for map elements or function return values
- Methods work on any named type, not just structs — enables adding behavior to slices, primitives, and aliases
Comments