The copy that never changes
You write a method to update a user's balance. You call the method. You print the balance. The number hasn't changed. You stare at the screen. The code looks right. The method ran. The math is correct. But the struct sitting in your main function is still holding the old value. You spent twenty minutes debugging a logic error that doesn't exist. The issue is the receiver. You passed a copy.
Go methods attach to types via receivers. A receiver is just the first argument of a function, written before the name. You can define a receiver as a value or a pointer. A value receiver gets a copy of the data. A pointer receiver gets the address of the data. The choice determines whether your method works on a duplicate or the original.
Think of a value receiver like handing someone a photocopy of a form. They can write on their copy all they want. It never changes the original form in your hand. A pointer receiver is like handing someone the original form. If they write on it, you see the changes immediately.
Minimal example: copy versus address
Here is the simplest case. A counter struct with two methods. One takes a value. One takes a pointer.
package main
import "fmt"
type Counter struct {
count int
}
// IncrementValue receives a copy of the struct.
func (c Counter) IncrementValue() {
// c is a local copy. Modifying it does not affect the caller.
c.count++
}
// IncrementPointer receives the address of the struct.
func (c *Counter) IncrementPointer() {
// c is a pointer. Dereferencing updates the original data.
c.count++
}
func main() {
c := Counter{count: 10}
// The method receives a copy. The original stays at 10.
c.IncrementValue()
fmt.Println(c.count) // prints: 10
// Go auto-dereferences. The original struct is modified.
c.IncrementPointer()
fmt.Println(c.count) // prints: 11
}
The value receiver method increments the copy. The copy disappears when the function returns. The pointer receiver method writes to the memory address. The change persists.
The receiver name usually matches the type with one or two letters. Use (c *Counter), not (this *Counter) or (self *Counter). This is a community convention. It keeps code compact and readable. Most editors run gofmt on save, which enforces consistent formatting around receivers. You don't need to argue about indentation.
How the compiler handles receivers
When you call c.IncrementValue(), the compiler generates code to copy the bytes of c onto the stack and pass that copy to the function. Inside the function, c refers to the copy. When the function returns, the copy is discarded. The original c in main was never touched.
With c.IncrementPointer(), the compiler passes the memory address. The function writes to that address. The change persists.
Go has a convenience feature that often hides the distinction. If you have a value and call a pointer method, Go automatically takes the address. If you have a pointer and call a value method, Go automatically dereferences. This transparency is helpful but dangerous. It lets you call pointer methods on values without thinking about addresses, which can lead to subtle bugs when the value is not addressable.
The auto-dereference works only when the value is addressable. If you try to call a pointer method on a map value or a literal, the compiler rejects it. Map values are temporary copies. They have no stable address. You get an error like cannot call pointer method on map value. The compiler knows there is nowhere to store the mutation.
Method sets and interfaces
The receiver choice affects which types can call the method and which interfaces the type implements. This is called the method set.
A type T has a method set containing all methods with a value receiver. A type *T has a method set containing all methods with a value receiver plus all methods with a pointer receiver.
This means *T can always call methods defined on T. T can only call methods defined on T. If you define a method with a pointer receiver, only *T can call it directly. Go's auto-dereference allows T to call pointer methods if T is addressable, but the method set rules for interfaces are stricter.
Interface satisfaction depends on the method set. A type implements an interface if its method set contains all the interface methods. If you define methods with pointer receivers, only *T implements the interface. T does not.
type Incrementer interface {
Increment()
}
type Counter struct {
count int
}
// Increment has a pointer receiver.
func (c *Counter) Increment() {
c.count++
}
func main() {
c := Counter{}
// *Counter implements Incrementer.
var i Incrementer = &c
i.Increment()
// Counter does not implement Incrementer.
// The compiler rejects this with:
// Counter does not implement Incrementer (Increment method has pointer receiver)
// var j Incrementer = c
}
The compiler complains with Counter does not implement Incrementer (Increment method has pointer receiver) if you try to assign a value to the interface. You must pass the address. This catches people off guard. You might have a struct with pointer methods and try to pass a value to a function expecting an interface. The code won't compile.
The convention "accept interfaces, return structs" helps here. When you return a struct, the caller decides whether to take the address. When you accept an interface, you accept both values and pointers, provided the method sets align. Design your interfaces with value receivers when possible so both T and *T satisfy them. This gives callers more flexibility.
Realistic example: large structs and mutexes
Real code often involves structs with many fields. Copying a large struct is expensive. Even if you don't modify it, passing a pointer avoids the copy cost.
type User struct {
ID int
Name string
Email string
// Large structs contain many fields. Copying all of them is slow.
History []Event
}
// UpdateEmail modifies the user, so it needs a pointer.
func (u *User) UpdateEmail(newEmail string) {
u.Email = newEmail
}
// FullName reads data. A value receiver works, but copying User is expensive.
// In practice, you'd likely use a pointer here too for performance.
func (u User) FullName() string {
return u.Name
}
Performance matters when the struct is large. If the struct fits in a few registers, copying is cheap. If it has dozens of fields or large slices, copying allocates memory and copies bytes. A pointer receiver passes a single address. The cost is constant regardless of struct size.
Strings are cheap to pass by value. A string in Go is just a pointer to a byte array plus a length. Passing *string is almost never needed and hurts readability. Stick to value receivers for small types like string, int, or small structs. Don't pass a pointer just because you're used to other languages. Go makes copies cheap for small data.
Mutexes and channels require pointer receivers. Copying a mutex breaks synchronization. If you copy a mutex, the copy is independent. Updates to the copy don't sync with the original. This leads to race conditions and deadlocks. The compiler allows copying a mutex, but the runtime behavior is wrong. You must use a pointer receiver to share the mutex state.
type SafeCounter struct {
mu sync.Mutex
count int
}
// Increment locks the mutex. A value receiver would copy the mutex,
// breaking synchronization. The pointer is mandatory here.
func (s *SafeCounter) Increment() {
s.mu.Lock()
defer s.mu.Unlock()
s.count++
}
The sync.Mutex field must be shared. A pointer receiver ensures all calls operate on the same mutex instance. This is a hard rule for concurrency primitives.
Pitfalls and errors
Pointer receivers can be nil. If you call a method on a nil pointer, the program panics. Value receivers never panic from nil because the value always exists. This is a subtle safety difference. If your method doesn't need to modify state, a value receiver protects you from nil panics.
Nil panics happen when you initialize a pointer but forget to allocate the struct.
type Config struct {
Host string
}
func (c *Config) SetHost(h string) {
// If c is nil, this line panics with:
// runtime error: invalid memory address or nil pointer dereference
c.Host = h
}
func main() {
var c *Config
// c is nil. This call panics.
c.SetHost("localhost")
}
The runtime panics with invalid memory address or nil pointer dereference. You must allocate the struct before calling pointer methods. Use c := &Config{} or c := new(Config).
Map values are another trap. Map values are not addressable. You cannot take the address of a map value. If you try to call a pointer method on a map value, the compiler rejects it.
type Item struct {
Name string
}
func (i *Item) Rename(n string) {
i.Name = n
}
func main() {
m := map[string]Item{
"a": {Name: "first"},
}
// Map values are copies. You cannot pass a pointer to a temporary copy.
// The compiler rejects this with:
// cannot call pointer method on m["a"]
// m["a"].Rename("second")
}
The compiler complains with cannot call pointer method on m["a"]. You must extract the value, modify it, and store it back. Or use a map of pointers. Map of pointers avoids the copy but introduces nil checks and allocation overhead. Pick the right tool for the job.
Decision: when to use which
Use a value receiver when the method reads data and the type is small. Use a value receiver when you want to ensure the method cannot mutate the caller's data. Use a value receiver for built-in types like int, string, and bool. Use a pointer receiver when the method modifies the receiver. Use a pointer receiver when the type is large and copying would be expensive. Use a pointer receiver when the type contains a sync.Mutex or channel, since copying these breaks their internal state. Use a pointer receiver when you need the method set to include pointer methods for interface satisfaction.
Pointers mutate. Values copy. Pick the one that matches your intent. If you can use a value receiver, use it. It's safer and clearer. Receiver choice is part of the API contract. Document it implicitly by choosing the right type.