Back to blog

Learning Go: Method - Pointer Receiver vs Value Receiver

gogolangprogrammingmethods

When writing methods in Go, one of the fundamental decisions you'll make is whether to use a pointer receiver or a value receiver. This choice impacts how your methods interact with data, affecting both performance and behavior.

What Are Methods in Go?

In Go, a method is a function with a special receiver argument. The receiver appears between the func keyword and the method name:

type Person struct {
    Name string
    Age  int
}
 
// Method with value receiver
func (p Person) GetName() string {
    return p.Name
}
 
// Method with pointer receiver
func (p *Person) SetName(name string) {
    p.Name = name
}

Value Receiver

A value receiver creates a copy of the struct when the method is called. Any modifications made inside the method don't affect the original struct.

type Counter struct {
    Count int
}
 
func (c Counter) Increment() {
    c.Count++ // This only modifies the copy!
}
 
func main() {
    counter := Counter{Count: 0}
    counter.Increment()
    fmt.Println(counter.Count) // Output: 0 (unchanged!)
}

When to Use Value Receivers

  1. The method doesn't modify the receiver - When you only need to read data
  2. Small structs - Copying is cheap for small data structures
  3. Immutability - When you want to ensure the original data remains unchanged
  4. Simple types - For built-in types like int, string, etc.
type Point struct {
    X, Y int
}
 
// Good use of value receiver - just reading values
func (p Point) Distance() float64 {
    return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}

Pointer Receiver

A pointer receiver passes a reference to the struct. The method can modify the original struct, and no copying occurs.

type Counter struct {
    Count int
}
 
func (c *Counter) Increment() {
    c.Count++ // This modifies the original!
}
 
func main() {
    counter := Counter{Count: 0}
    counter.Increment()
    fmt.Println(counter.Count) // Output: 1 (modified!)
}

When to Use Pointer Receivers

  1. The method modifies the receiver - When you need to change the struct's state
  2. Large structs - Avoid expensive copy operations
  3. Consistency - If some methods need pointers, use pointers for all methods on that type
  4. Mutexes or other sync primitives - These must not be copied
type Account struct {
    Balance float64
    mu      sync.Mutex
}
 
// Must use pointer receiver - modifies state and contains mutex
func (a *Account) Deposit(amount float64) {
    a.mu.Lock()
    defer a.mu.Unlock()
    a.Balance += amount
}
 
func (a *Account) GetBalance() float64 {
    a.mu.Lock()
    defer a.mu.Unlock()
    return a.Balance
}

Key Differences

AspectValue ReceiverPointer Receiver
ModificationCannot modify originalCan modify original
MemoryCreates a copyUses reference
PerformanceSlower for large structsFaster for large structs
nil safetyCannot be called on nilCan be called on nil
Thread safetySafer (immutable)Requires synchronization

Important Gotcha: nil Pointer Receivers

Pointer receivers can be called even when the receiver is nil. You need to handle this explicitly:

type Tree struct {
    Value int
    Left  *Tree
    Right *Tree
}
 
func (t *Tree) Insert(value int) {
    if t == nil {
        // Cannot modify nil receiver!
        // This is a common mistake
        return
    }
    // ... insertion logic
}

Best Practices

  1. Be consistent - If any method needs a pointer receiver, use pointer receivers for all methods on that type
  2. Consider the semantics - Does your type represent a value (like time.Time) or a resource (like os.File)?
  3. Document your choice - Make it clear in comments whether a type should be copied or referenced
  4. Default to pointer receivers - When in doubt, pointer receivers are more flexible
// Good: Consistent pointer receivers
type User struct {
    ID   int
    Name string
}
 
func (u *User) SetName(name string) {
    u.Name = name
}
 
func (u *User) GetID() int {
    return u.ID // Still uses pointer receiver for consistency
}

Real-World Example

Here's a practical example showing both approaches:

package main
 
import "fmt"
 
// Value type - immutable point
type Point struct {
    X, Y float64
}
 
// Value receiver - returns new Point
func (p Point) Add(other Point) Point {
    return Point{X: p.X + other.X, Y: p.Y + other.Y}
}
 
// Reference type - mutable rectangle
type Rectangle struct {
    TopLeft     Point
    BottomRight Point
}
 
// Pointer receiver - modifies in place
func (r *Rectangle) Move(dx, dy float64) {
    r.TopLeft.X += dx
    r.TopLeft.Y += dy
    r.BottomRight.X += dx
    r.BottomRight.Y += dy
}
 
// Pointer receiver - calculates area (consistent with Move)
func (r *Rectangle) Area() float64 {
    width := r.BottomRight.X - r.TopLeft.X
    height := r.BottomRight.Y - r.TopLeft.Y
    return width * height
}
 
func main() {
    p1 := Point{X: 1, Y: 2}
    p2 := Point{X: 3, Y: 4}
    p3 := p1.Add(p2)
    fmt.Printf("Sum: (%v, %v)\n", p3.X, p3.Y)
 
    rect := Rectangle{
        TopLeft:     Point{X: 0, Y: 0},
        BottomRight: Point{X: 10, Y: 10},
    }
    fmt.Printf("Area: %v\n", rect.Area())
    rect.Move(5, 5)
    fmt.Printf("After move: %+v\n", rect)
}

Conclusion

Choosing between pointer and value receivers is about understanding your data's semantics:

  • Use value receivers for small, immutable types where copying is cheap
  • Use pointer receivers for types that should be modified, large structs, or when consistency demands it

When in doubt, prefer pointer receivers—they're more flexible and prevent common bugs related to unintentional copying.

Happy coding in Go! 🚀


Go Learning Roadmap:

Other Go Posts:


References

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.