Value Receivers and Pointer Receivers#
Functions that specify a receiver are called methods. The receiver can be a value receiver Animal
or a pointer receiver *Animal
:
type Animal struct {
name string
}
func (a Animal) valueFn() {} // Value receiver
func (a *Animal) ptrFn() {} // Pointer receiver
When calling a method, the caller can be either a value type or a pointer type of that type.
- With a value receiver, regardless of whether the caller is a value type or a pointer type, a copy of the caller is always passed, and the method does not modify the caller itself:
// eg1.
func (a Animal) valueFn() {
a.name = "new_" + a.name
}
func main() {
a := Animal{"cat"} // Value type
a.valueFn()
println("this is name", a.name)
// => this is name cat
b := &Animal{"dog"} // Pointer type
b.valueFn()
println("this is name", b.name)
// => this is name dog
}
- With a pointer receiver, a reference to the caller is passed, so modifications can be made to the caller:
// eg2.
func (a *Animal) ptrFn() {
a.name = "new_" + a.name
}
func main() {
a := Animal{"cat"} // Value type
a.ptrFn()
println("this is name", a.name)
// => this is name new_cat
b := &Animal{"dog"} // Pointer type
b.ptrFn()
println("this is name", b.name)
// => this is name new_dog
}
This is achieved by the compiler, as shown in the table below:
- | Value Receiver | Pointer Receiver |
---|---|---|
Value Type Caller | The method uses a copy of the caller, similar to "pass by value" | The method is called using a reference to the value, eg2 is actually (&a).ptrFn() |
Pointer Type Caller | The pointer is dereferenced to a value, eg1 is actually (*b).valueFn() | It is also "pass by value" in practice, the operations inside the method will affect the caller, similar to passing a pointer, a copy of the pointer is made |
The Essence of Types#
After declaring a new type and before declaring a method for that type, you need to answer one question: What is the essence of this type? If you want to add or remove a value from this type, do you want to create a new value or modify the current value? If you want to create a new value, the methods of this type should use value receivers. If you want to modify the current value, use pointer receivers. This answer will also affect how values of this type are passed within the program: whether they are passed by value or by pointer. It is important to maintain consistency in the way values are passed. The underlying principle behind this is not to focus only on how a method handles a value, but to focus on the essence of the value itself.
Whether to use a value receiver or a pointer receiver should not be determined by whether the method modifies the received value. This decision should be based on the essence of the type. An exception to this rule is when you need the type value to conform to an interface. Even if the essence of the type is non-primitive, you can still choose to declare methods with value receivers. This approach fully complies with the mechanism of calling methods on interface values.
- "Go in Action 5.3"
- Built-in types: Numeric types, string types, and boolean types. These types are essentially primitive types. Therefore, when adding or removing values from these types, a new value is created. Based on this conclusion, when passing values of these types to methods or functions, a copy of the corresponding value should be passed.
- Primitive types: Slice, map, channel, interface, and function types. I didn't understand the rest 😅
- Struct types: Used to describe a group of values. Follow the rule of passing by pointer if modifications are needed. If the value of a type has a non-primitive essence, it should be shared rather than copied. Even if the method does not modify the receiver's value, it should still be declared with a pointer receiver.
I didn't understand this section very well, but it seems to be saying that for non-struct types, the conventions of the standard library should be followed; for struct types, in general, if modifications to the caller are not needed, pass by value, and if modifications are needed, pass by pointer.
Interfaces#
Although methods with value receivers and methods with pointer receivers can be called on callers of different types, there are slight differences in implementing interfaces. Implementing a method with a value receiver implicitly implements the method with a pointer receiver, while implementing a method with a pointer receiver does not automatically implement the method with a value receiver.
// eg3. Compiles successfully
type IAnimal interface {
valueFn()
ptrFn()
}
func (a Animal) valueFn() {}
func (a *Animal) ptrFn() {}
func main() {
var a IAnimal = &Animal{"cat"}
a.valueFn()
a.ptrFn()
}
// eg4. Compilation fails
func main() {
var a IAnimal = Animal{"cat"} // Changed to value type
a.valueFn()
a.ptrFn()
}
// => .\main.go:22:18: cannot use Animal{…} (value of type Animal) as IAnimal value in variable declaration: Animal does not implement IAnimal (method ptrFn has pointer receiver)
Method Sets#
To understand why a value of type Animal
cannot implement the IAnimal
interface when implementing it with a pointer receiver, we need to understand method sets. A method set defines a set of methods associated with a given type's values or pointers. The type of the receiver used when defining a method determines whether the method is associated with a value, a pointer, or both.
- Method sets as described in the specification:
Values | Method Receivers |
---|---|
T | (t T) |
* T | (t T) and (t * T) |
This means that the method set of a value of type T
only includes methods declared with value receivers. The method set of a pointer to type T
includes both methods declared with value receivers and methods declared with pointer receivers.
- Method sets from the perspective of the receiver:
Method Receivers | Values |
---|---|
(t T) | T and * T |
(t * T) | * T |
This perspective states that if a value receiver is used to implement an interface, both a value type value and a pointer type value can implement the corresponding interface. If a pointer receiver is used to implement an interface, only a pointer to that type can implement the corresponding interface.
Therefore, in eg4, the value type Animal
will prompt that it does not implement the IAnimal
interface.
Embedded Types#
Embedded types are existing types declared directly within a new struct type. The embedded type is referred to as the inner type of the new outer type.
type Cat struct {
Animal
color string
}
func main() {
c := Cat{Animal{"meow"},"yellow"}
c.Animal.valueFn()
}
The properties and methods of the inner type can also be promoted to the outer type for direct access:
func main() {
//...
c.valueFn()
println("this is c name", c.name)
}
References#
- Chapter 5 of "Go in Action"
- Difference between value receivers and pointer receivers | Go Programmer Interview Written Test Compendium