Navigation

Go

Go Method Receivers: Value vs Pointer Receivers Explained (2025)

Master Go method receivers with comprehensive guide on value vs pointer receivers. Learn when to use each type, performance implications, and best practices for Go development.

Table Of Contents

Introduction

One of the most fundamental decisions when writing methods in Go is choosing between value and pointer receivers. This choice affects performance, mutability, memory usage, and the overall design of your types. Understanding when and why to use each type of receiver is crucial for writing efficient and maintainable Go code.

Whether you're building APIs, working with large data structures, or implementing interfaces, the receiver type you choose can significantly impact your application's behavior and performance. In this comprehensive guide, you'll learn the key differences, performance implications, and best practices for using method receivers effectively.

Understanding Method Receivers

Basic Receiver Syntax

In Go, methods are functions with a special receiver argument that appears between the func keyword and the method name:

type Rectangle struct {
    Width  float64
    Height float64
}

// Value receiver
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Pointer receiver
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    
    fmt.Println("Area:", rect.Area())        // 50
    rect.Scale(2)
    fmt.Println("After scaling:", rect.Area()) // 200
}

The Key Difference

The fundamental difference between value and pointer receivers lies in what gets passed to the method:

  • Value receiver: A copy of the value is passed
  • Pointer receiver: A pointer to the original value is passed
type Counter struct {
    value int
}

// Value receiver - operates on a copy
func (c Counter) IncrementValue() {
    c.value++  // This only affects the copy
}

// Pointer receiver - operates on the original
func (c *Counter) IncrementPointer() {
    c.value++  // This affects the original
}

func main() {
    counter := Counter{value: 0}
    
    counter.IncrementValue()
    fmt.Println("After IncrementValue:", counter.value)  // 0 (unchanged)
    
    counter.IncrementPointer()
    fmt.Println("After IncrementPointer:", counter.value) // 1 (changed)
}

When to Use Value Receivers

1. Small, Immutable Types

Value receivers are ideal for small types that don't need to be modified:

type Point struct {
    X, Y float64
}

func (p Point) Distance(other Point) float64 {
    dx := p.X - other.X
    dy := p.Y - other.Y
    return math.Sqrt(dx*dx + dy*dy)
}

func (p Point) String() string {
    return fmt.Sprintf("Point(%.2f, %.2f)", p.X, p.Y)
}

func (p Point) Add(other Point) Point {
    return Point{X: p.X + other.X, Y: p.Y + other.Y}
}

2. Methods That Don't Modify State

When your method only reads data without modifying it:

type User struct {
    FirstName string
    LastName  string
    Email     string
    Age       int
}

func (u User) FullName() string {
    return u.FirstName + " " + u.LastName
}

func (u User) IsAdult() bool {
    return u.Age >= 18
}

func (u User) EmailDomain() string {
    parts := strings.Split(u.Email, "@")
    if len(parts) == 2 {
        return parts[1]
    }
    return ""
}

3. Basic Types and Small Structs

For primitive types and small structs (typically less than a few words):

type Temperature float64

func (t Temperature) Celsius() float64 {
    return float64(t)
}

func (t Temperature) Fahrenheit() float64 {
    return float64(t)*9/5 + 32
}

func (t Temperature) Kelvin() float64 {
    return float64(t) + 273.15
}

type RGB struct {
    R, G, B uint8
}

func (c RGB) Hex() string {
    return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}

func (c RGB) Grayscale() RGB {
    gray := uint8((int(c.R) + int(c.G) + int(c.B)) / 3)
    return RGB{R: gray, G: gray, B: gray}
}

When to Use Pointer Receivers

1. Methods That Modify the Receiver

When you need to modify the receiver's state:

type BankAccount struct {
    balance float64
    owner   string
}

func (ba *BankAccount) Deposit(amount float64) error {
    if amount <= 0 {
        return errors.New("deposit amount must be positive")
    }
    ba.balance += amount
    return nil
}

func (ba *BankAccount) Withdraw(amount float64) error {
    if amount <= 0 {
        return errors.New("withdrawal amount must be positive")
    }
    if amount > ba.balance {
        return errors.New("insufficient funds")
    }
    ba.balance -= amount
    return nil
}

func (ba *BankAccount) Balance() float64 {
    return ba.balance  // Even getters often use pointer receivers for consistency
}

2. Large Structs

To avoid copying large amounts of data:

type Image struct {
    Width  int
    Height int
    Pixels [][]RGB  // Potentially very large
    Metadata map[string]string
}

// Pointer receiver to avoid copying the entire image
func (img *Image) Resize(newWidth, newHeight int) {
    // Resize logic here
    newPixels := make([][]RGB, newHeight)
    for i := range newPixels {
        newPixels[i] = make([]RGB, newWidth)
    }
    
    // Bilinear interpolation or other resizing algorithm
    scaleX := float64(img.Width) / float64(newWidth)
    scaleY := float64(img.Height) / float64(newHeight)
    
    for y := 0; y < newHeight; y++ {
        for x := 0; x < newWidth; x++ {
            srcX := int(float64(x) * scaleX)
            srcY := int(float64(y) * scaleY)
            if srcX < img.Width && srcY < img.Height {
                newPixels[y][x] = img.Pixels[srcY][srcX]
            }
        }
    }
    
    img.Width = newWidth
    img.Height = newHeight
    img.Pixels = newPixels
}

func (img *Image) AddMetadata(key, value string) {
    if img.Metadata == nil {
        img.Metadata = make(map[string]string)
    }
    img.Metadata[key] = value
}

3. Interface Consistency

When some methods need pointer receivers, use pointer receivers for all methods:

type Database interface {
    Connect() error
    Disconnect() error
    Query(sql string) ([]Row, error)
    Execute(sql string) error
}

type PostgresDB struct {
    connectionString string
    conn            *sql.DB
    isConnected     bool
}

// All methods use pointer receivers for consistency
func (db *PostgresDB) Connect() error {
    conn, err := sql.Open("postgres", db.connectionString)
    if err != nil {
        return err
    }
    db.conn = conn
    db.isConnected = true
    return nil
}

func (db *PostgresDB) Disconnect() error {
    if db.conn != nil {
        err := db.conn.Close()
        db.conn = nil
        db.isConnected = false
        return err
    }
    return nil
}

func (db *PostgresDB) Query(sqlQuery string) ([]Row, error) {
    if !db.isConnected {
        return nil, errors.New("database not connected")
    }
    // Query implementation
    return nil, nil
}

func (db *PostgresDB) Execute(sqlQuery string) error {
    if !db.isConnected {
        return errors.New("database not connected")
    }
    // Execute implementation
    return nil
}

Performance Implications

Memory Allocation and Copying

Value receivers create copies, which can be expensive for large structs:

type LargeStruct struct {
    data [1000000]int  // 8MB of data
}

// Expensive - copies 8MB every time it's called
func (ls LargeStruct) ProcessValue() {
    // Process data
}

// Efficient - only passes 8 bytes (pointer size)
func (ls *LargeStruct) ProcessPointer() {
    // Process data
}

func BenchmarkValueReceiver(b *testing.B) {
    ls := LargeStruct{}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ls.ProcessValue()
    }
}

func BenchmarkPointerReceiver(b *testing.B) {
    ls := LargeStruct{}
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ls.ProcessPointer()
    }
}

Garbage Collector Impact

Pointer receivers can affect GC performance differently:

// Value receiver - less GC pressure as no additional pointers
func (p Point) Transform() Point {
    return Point{X: p.X * 2, Y: p.Y * 2}
}

// Pointer receiver - may create more work for GC
func (p *Point) TransformInPlace() {
    p.X *= 2
    p.Y *= 2
}

Advanced Patterns and Best Practices

1. Consistent Receiver Types

Choose one receiver type for all methods on a type:

// Good: All methods use pointer receivers
type Server struct {
    port int
    host string
}

func (s *Server) Start() error { /* ... */ }
func (s *Server) Stop() error { /* ... */ }
func (s *Server) Status() string { /* ... */ }

// Bad: Mixed receiver types
type Server struct {
    port int
    host string
}

func (s *Server) Start() error { /* ... */ }  // Pointer receiver
func (s Server) Status() string { /* ... */ }  // Value receiver - inconsistent!

2. Interface Implementation Considerations

Be aware of how receiver types affect interface satisfaction:

type Formatter interface {
    Format() string
}

type Document struct {
    title   string
    content string
}

// Pointer receiver method
func (d *Document) Format() string {
    return fmt.Sprintf("Title: %s\nContent: %s", d.title, d.content)
}

func main() {
    // This works
    doc := &Document{title: "Test", content: "Content"}
    var f Formatter = doc
    fmt.Println(f.Format())
    
    // This doesn't work - value doesn't implement interface with pointer receiver
    // doc2 := Document{title: "Test", content: "Content"}
    // var f2 Formatter = doc2  // Compile error!
    
    // But this works - Go automatically takes the address
    doc3 := Document{title: "Test", content: "Content"}
    var f3 Formatter = &doc3
    fmt.Println(f3.Format())
}

3. Method Sets and Addressability

Understanding method sets is crucial:

type Calculator struct {
    result float64
}

func (c Calculator) Add(n float64) Calculator {
    c.result += n
    return c
}

func (c *Calculator) AddInPlace(n float64) {
    c.result += n
}

func main() {
    // Value method set includes both value and pointer receiver methods
    calc := Calculator{}
    calc.Add(5)        // Works
    calc.AddInPlace(5) // Works (Go automatically takes address)
    
    // Pointer method set includes both value and pointer receiver methods
    calcPtr := &Calculator{}
    calcPtr.Add(5)        // Works (Go automatically dereferences)
    calcPtr.AddInPlace(5) // Works
    
    // But be careful with non-addressable values
    result := Calculator{}.Add(10)  // Works
    // Calculator{}.AddInPlace(10)  // Compile error! Can't take address of literal
}

4. Functional Programming Patterns

Value receivers work well with functional programming patterns:

type List []int

// Functional style with value receivers
func (l List) Map(fn func(int) int) List {
    result := make(List, len(l))
    for i, v := range l {
        result[i] = fn(v)
    }
    return result
}

func (l List) Filter(fn func(int) bool) List {
    result := List{}
    for _, v := range l {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

func (l List) Reduce(fn func(int, int) int, initial int) int {
    result := initial
    for _, v := range l {
        result = fn(result, v)
    }
    return result
}

func main() {
    numbers := List{1, 2, 3, 4, 5}
    
    doubled := numbers.Map(func(x int) int { return x * 2 })
    evens := doubled.Filter(func(x int) bool { return x%2 == 0 })
    sum := evens.Reduce(func(a, b int) int { return a + b }, 0)
    
    fmt.Println(sum) // 30
}

Common Pitfalls and Solutions

1. Accidentally Using Value Receivers for Large Structs

// Problematic: Copying large struct
type APIResponse struct {
    Data    []map[string]interface{} // Potentially large
    Headers map[string]string
    Status  int
}

func (r APIResponse) ProcessResponse() { // Expensive copy!
    // Process response
}

// Solution: Use pointer receiver
func (r *APIResponse) ProcessResponse() {
    // Process response efficiently
}

2. Inconsistent Receiver Types

// Problematic: Mixed receiver types
type User struct {
    name  string
    email string
}

func (u User) Name() string    { return u.name }     // Value receiver
func (u *User) SetName(name string) { u.name = name } // Pointer receiver
func (u User) Email() string   { return u.email }    // Value receiver again

// Solution: Use consistent pointer receivers
func (u *User) Name() string           { return u.name }
func (u *User) SetName(name string)    { u.name = name }
func (u *User) Email() string          { return u.email }
func (u *User) SetEmail(email string)  { u.email = email }

3. Interface Implementation Issues

type Writer interface {
    Write([]byte) error
}

type FileWriter struct {
    filename string
    file     *os.File
}

// Pointer receiver
func (fw *FileWriter) Write(data []byte) error {
    return fw.file.Write(data)
}

func main() {
    // This won't work
    var w Writer = FileWriter{filename: "test.txt"}  // Error!
    
    // This works
    var w Writer = &FileWriter{filename: "test.txt"} // OK
}

Testing Receiver Types

Performance Benchmarks

func BenchmarkValueReceiver(b *testing.B) {
    s := LargeStruct{data: [10000]int{}}
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        s.ValueMethod()
    }
}

func BenchmarkPointerReceiver(b *testing.B) {
    s := LargeStruct{data: [10000]int{}}
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        s.PointerMethod()
    }
}

Testing Mutability

func TestValueReceiverImmutability(t *testing.T) {
    counter := Counter{value: 5}
    original := counter.value
    
    counter.IncrementValue()
    
    if counter.value != original {
        t.Errorf("Value receiver should not modify original: got %d, want %d", 
                 counter.value, original)
    }
}

func TestPointerReceiverMutability(t *testing.T) {
    counter := Counter{value: 5}
    original := counter.value
    
    counter.IncrementPointer()
    
    if counter.value == original {
        t.Errorf("Pointer receiver should modify original: got %d, want %d", 
                 counter.value, original+1)
    }
}

FAQ

Q: Should I always use pointer receivers for consistency? A: Not always. For small, immutable types, value receivers are often more appropriate and can be more efficient.

Q: Can I mix value and pointer receivers on the same type? A: While technically possible, it's generally not recommended as it can be confusing and lead to unexpected behavior.

Q: Do pointer receivers always perform better? A: No. For small types, value receivers can be faster due to better cache locality and avoiding pointer indirection.

Q: How do I choose between value and pointer receivers? A: Use pointer receivers when you need to modify the receiver, for large structs, or for interface consistency. Use value receivers for small, immutable types.

Q: What about embedded types and receiver types? A: Embedded types inherit the method set from their receiver types, so be consistent with your choices.

Q: Can interfaces specify receiver types? A: No, interfaces only specify method signatures. The receiver type affects which types can implement the interface.

Conclusion

Choosing between value and pointer receivers is a fundamental decision that affects performance, behavior, and API design in Go. Understanding the trade-offs helps you make informed decisions that lead to more efficient and maintainable code.

Key guidelines:

  • Use pointer receivers for methods that modify the receiver
  • Use pointer receivers for large structs to avoid copying
  • Use value receivers for small, immutable types
  • Be consistent within a type - don't mix receiver types
  • Consider interface implementation requirements
  • Profile performance when in doubt

Master these patterns to write more effective Go code that's both performant and idiomatic. Share your experiences with receiver types and any patterns you've discovered in the comments below!

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Go