Understanding Value and Pointer Receivers in Go Interfaces

Alexander Fernandez
6 min readNov 13, 2024

--

Go (Golang) is a statically typed, compiled programming language designed with simplicity and efficiency in mind. One of its core features is the concept of interfaces, which allow different types to be used interchangeably based on the methods they implement.

But, sometimes you can see Golang code like this:

type Dog struct {}

func (d Dog) Bark() {
return "barking..."
}

And others like this:

type Dog struct {}

func (d *Dog) Bark() {
return "barking..."
}

A nuanced aspect of Go is how methods are associated with types — specifically, whether they have value receivers or pointer receivers. This distinction significantly impacts how types satisfy interfaces and how they behave in code.

In this article, we’ll delve deep into:

• The difference between value and pointer receivers.

• How receiver types affect interface implementation.

• Best practices for choosing receiver types.

• Practical examples illustrating these concepts.

1. Value vs. Pointer Receivers

In Go, methods can be attached to types in two ways:

Value Receiver: The method receives a copy of the type’s value.

Pointer Receiver: The method receives a pointer to the type’s value.

This distinction influences method behavior, memory efficiency, and interface satisfaction.

Value Receivers

A method with a value receiver operates on a copy of the value. Any modifications made within the method do not affect the original value.

Example:

type Counter struct {
value int
}

func (c Counter) Increment() {
c.value++
}

In this example, calling Increment on a Counter value will increment the copy’s value, leaving the original Counter unchanged.

Pointer Receivers

A method with a pointer receiver operates on the original value via its memory address. Modifications within the method affect the original value.

Example:

func (c *Counter) Increment() {
c.value++
}

Here, calling Increment on a Counter pointer modifies the original Counter’s value.

2. Interfaces and Receiver Types

An interface in Go specifies a set of method signatures. Any type that implements these methods satisfies the interface. However, the receiver type (value or pointer) plays a crucial role in whether a type satisfies an interface.

How Receiver Types Affect Interface Satisfaction

Methods with Value Receivers:

  • Both the type and its pointer implement the interface.

Methods with Pointer Receivers:

  • Only the pointer to the type implements the interface.

Why?

This behavior is due to how Go defines the method sets for types and their pointers:

Type T:

  • Method set includes all methods with value receivers.

Pointer to T (*T):

  • Method set includes all methods with value receivers and pointer receivers.

Examples with Code

Let’s explore this with code examples.

Defining an Interface:

type Animal interface {
Sound() string
}

Implementing with Value Receiver:

type Dog struct{}

func (d Dog) Sound() string {
return "Woof!"
}

Method Set of Dog:

  • Sound() string

Method Set of *Dog:

  • Sound() string (methods with value receivers are promoted to pointer types)

Both Dog and *Dog satisfy Animal.

Implementing with Pointer Receiver:

type Cat struct{}

func (c *Cat) Sound() string {
return "Meow!"
}

Method Set of Cat:

  • (empty, as methods with pointer receivers are not included)

Method Set of *Cat:

  • Sound() string

Only *Cat satisfies Animal.

Demonstration in main:

func main() {
var myPet Animal

// Works because Dog{} implements Animal
myPet = Dog{}
fmt.Println(myPet.Sound()) // Outputs: Woof!

// Also works
myPet = &Dog{}
fmt.Println(myPet.Sound()) // Outputs: Woof!

// Error: Cat{} does not implement Animal
// myPet = Cat{} // Uncommenting this line causes a compile-time error

// Works because &Cat{} (type *Cat) implements Animal
myPet = &Cat{}

fmt.Println(myPet.Sound()) // Outputs: Meow!
}

3. When to Use Value or Pointer Receivers

Choosing between value and pointer receivers depends on several factors, including the method’s purpose, performance considerations, and expectations for how the type should be used.

Guidelines for Choosing Receiver Types

1. Modifying the Receiver:

  • Use a pointer receiver if the method needs to modify the receiver’s fields.
  • Example:
func (c *Counter) Increment() {
c.value++
}

2. Avoiding Copying Large Structs:

  • Use a pointer receiver to avoid copying large structs, which can be inefficient.
  • Example:
type LargeStruct struct {
Data [1000000]int
}

func (ls *LargeStruct) Process() {
// Processing data
}

3. Consistency:

  • Keep the receiver type consistent across methods for the same type.
  • If some methods require a pointer receiver, it’s often best to make all methods use pointer receivers to avoid confusion.

4. Value Semantics:

  • Use a value receiver when the method does not modify the receiver and the receiver is small (e.g., basic types, small structs).
  • Example:
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}

5. Interface Satisfaction:

  • If you want both the value and pointer types to satisfy an interface, ensure that all methods in the interface are implemented with value receivers.

4. Common Pitfalls and How to Avoid Them

Understanding how method sets work in Go is essential to avoid common mistakes, especially when working with interfaces.

Mistakes in Interface Implementations

Mistake: Assuming that a type with methods having pointer receivers satisfies an interface when used as a value.

Example:

type Printer interface {
Print()
}

type Document struct{}

func (d *Document) Print() {
fmt.Println("Printing document…")
}

func main() {
var p Printer
doc := Document{}
p = doc // Error: Document does not implement Printer
}

Solution:

  • Use a pointer to Document:
p = &doc // Works because *Document implements Printer

• Or change the method to have a value receiver:

func (d Document) Print() {
fmt.Println("Printing document…")
}

Understanding Method Sets

Definition of Method Sets:

Type T:

  • Methods declared with receiver type T.

Pointer to T (*T):

  • Methods declared with receiver type T or *T.

Implications:

  • Methods with pointer receivers are not part of the method set of the value type.
  • Methods with value receivers are part of the method sets of both the value and pointer types.

Visual Representation:

Type T:
- Methods with receiver (t T)
Pointer to T (*T):
- Methods with receiver (t T)
- Methods with receiver (t *T)

Key Takeaways:

Value Type (T):

  • Can’t access methods with pointer receivers.

Pointer Type (*T):

  • Can access all methods, whether they have value or pointer receivers.

5. Conclusion

Understanding the nuances between value and pointer receivers in Go is crucial for writing effective and bug-free code. The choice of receiver type affects:

• Whether a type satisfies an interface.

• How methods interact with the receiver (modifying original data vs. working on a copy).

• Performance implications due to copying large structs.

Best Practices:

• Use value receivers for small structs and when methods do not modify the receiver.

• Use pointer receivers when methods need to modify the receiver or when working with large structs.

• Keep receiver types consistent across methods for a given type.

• Be mindful of method sets and how they affect interface satisfaction.

By adhering to these guidelines, you can ensure that your Go code is efficient, readable, and functions as intended.

6. References

The Go Programming Language Specification

Effective Go — Methods

Go Blog — Method Sets

Go Tour — Methods and Interfaces

Appendix: Complete Example Code

package main

import "fmt"

// The Animal interface
type Animal interface {
Sound() string
}

// Define a Dog with a value receiver
type Dog struct{}

func (d Dog) Sound() string {
return "Woof!"
}

// Define a Cat with a pointer receiver
type Cat struct{}

func (c *Cat) Sound() string {
return "Meow!"
}

func main() {
var myPet Animal

// Dog{} implements Animal
myPet = Dog{}
fmt.Println(myPet.Sound()) // Outputs: Woof!

// Also works
myPet = &Dog{}
fmt.Println(myPet.Sound()) // Outputs: Woof!

// &Cat{} (type *Cat) implements Animal
myPet = &Cat{}
fmt.Println(myPet.Sound()) // Outputs: Meow!

// Error: Cat{} does not implement Animal
// myPet = Cat{} // Uncommenting this line will cause a compile-time error
}

Final Thoughts

By mastering the concept of receiver types in Go, you enhance your ability to design robust interfaces and types. Remember, the key lies in understanding how method sets work and how they influence interface implementation. Happy coding!

Note: All code examples are tested with Go 1.23

--

--

Alexander Fernandez
Alexander Fernandez

No responses yet