本篇文章整理值接收器与指针接收器相关知识。
方法
Golang 方法能给用户自定义的类型添加新的行为,它和函数的区别在于方法跟类型相关联,有一个特殊的 receiver 参数。
type Dog struct {
Name string
}
// 方法
func (d Dog) GetName() string {
return d.Name
}
// 函数
func GetName(d Dog) string {
return d.Name
}
“接收器”名词解释(receiver)
type Rectangle struct {
width float64
height float64
}
// 值接收器方法
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// 指针接收器方法
func (r *Rectangle) Scale(factor float64) {
r.width *= factor
r.height *= factor
}
func (r Rectangle) Area() float64func (r *Rectangle) Scale(s float64)
本文将统一使用以上翻译。
修改结构体值需要使用指针接收器方法
值接收器代码示例
package main
import "fmt"
type Dog struct {
Name string
}
func (d Dog) GetName() string {
return d.Name
}
func (d Dog) SetName(name string) {
d.Name = name
}
func main() {
d := Dog{Name: "Charlie"}
fmt.Println(d.GetName())
d.SetName("Luna")
fmt.Println(d.GetName())
}
输出如下
Charlie
Charlie
调用值接收器方法会触发 Dog 结构体复制,对副本的修改不会改变 Dog 实例的值。
type Dog struct {
Name string
}
func (d *Dog) GetName() string {
return d.Name
}
func (d *Dog) SetName(name string) {
d.Name = name
}
T*T
Charlie
Luna
指针接收器会触发复制指针,不过复制的指针也还是指向原数据,所以可以修改数据值。
值类型不能调用指针接收器方法
此处将 GetName 实现为值接收器方法,而将 SetName() 实现为指针接收器方法。(实际开发中应保持接收器类型相同,不应混用)
type Dog struct {
Name string
}
func (d Dog) GetName() string {
return d.Name
}
func (d *Dog) SetName(name string) {
d.Name = name
}
func main() {
Dog{Name: "Charlie"}.GetName() // ok
Dog{Name: "Charlie"}.SetName("Luna") // 报错:cannot call pointer method SetName on Dog
}
Dog{Name: "Charlie"}
更详细的解释是它是右值表达式,还未存储到可寻址变量,只有当其赋值给变量存储在内存中时,才能对它进行寻址操作。
且看赋值后的代码如下:
func main() {
dog := Dog{Name: "Charlie"}
dog.GetName() // ok
dog.SetName("Luna") // ok
}
为什么赋值到 dog 变量后,SetName 方法就能用了呢?这是因为在 可寻址值变量(左值) 上调用指针接收器方法,Golang 会自动隐式的为变量取地址后调用方法,Golang 的语法糖让我们不用再啰嗦的显式取地址。同理,当指针类型调用值接收器方法时,Golang 也会通过指针找到值类型,在值类型上调用方法。
// 值类型调用指针接收器方法
dog := Dog{Name: "Charlie"}
dog.SetName("Luna") // 编译器会处理为 (&dog).SetName("Luna")
// 指针类型调用值接收器方法
dog := &Dog{Name: "Charlie"}
dog.GetName() // 编译器会处理为 (*dog).SetName("Luna")
在官方 effective go 文档中,对两者区别有精确的描述:
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.
There is a handy exception, though. When the value is addressable, the language takes care of the common case of invoking a pointer method on a value by inserting the address operator automatically.
意思是:
值方法(value methods)可以通过指针和值调用,但是指针方法(pointer methods)只能通过指针来调用。 但有一个例外,如果某个值是可寻址的(addressable,或者说左值),那么编译器会在值调用指针方法时自动插入取地址符,使得在此情形下看起来像指针方法也可以通过值来调用。
类型与接收器方法的调用关系
dog.SetName("Luna")(&dog).SetName("Luna")dog.GetName()(*dog).SetName("Luna")
从逻辑上理解为什么 “值类型不能调用指针接收器方法”
指针接收器方法,很可能在方法中会对调用者的属性进行更改操作,从而影响接收器;而对于值接收器方法,在方法中不会对接收器本身产生影响。
所以,当实现了一个值接收器方法,就可以自动生成一个指针接收器方法,因为两者都不会影响接收器。但是,当实现了一个指针接收器方法,如果此时自动生成一个值接收器方法,原本期望对接收器的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响接收器。
可以简单记住下面这点:
如果实现了值接收器方法,会隐含地也实现了指针接收器方法。
补充:本段描述中的 “接收器” 可能不如 “调用者” 好理解,不过 “接收器” 是更规范的称呼。
Go 语言中的方法(method)是一种作用于特定类型变量的函数。 这种特定类型变量叫做接收器(receiver)。 如果将特定类型理解为结构体或类,那接收器的概念就类似于其他语言中的 this 或者 self,在 Go 语言中,接收器的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法,但必须是通过type定义的类型。
接收器方法与接口实现
一个类型可以有值接收器方法集和指针接收器方法集,值接收器方法集是指针接收器方法集的子集,反之不是
值对象只可以使用值接收器方法集,指针对象可以使用 值接收器方法集 + 指针接收器方法集
接口的匹配 (或者叫实现),类型实现了接口的所有方法,叫匹配,具体的讲,要么是类型的值方法集匹配接口,要么是指针方法集匹配接口,具体的匹配分两种:
值方法集和接口匹配:给接口变量赋值的不管是值还是指针对象,都 ok,因为都包含值方法集 指针方法集和接口匹配:只能将指针对象赋值给接口变量,因为只有指针方法集和接口匹配,如果将值对象赋值给接口变量,会在编译期报错 (会触发接口合理性检查机制)
有了之前的基础,从合理性角度就很好理解,如果我们实现了指针接收器方法和接口匹配,并且在方法中使用指针修改了接收器,如果允许将值类型赋值给接口变量,再通过接口变量调用方法,值类型和指针类型执行的结果就会不一致,这显然是不合理的。
其实只要记住:指针接收器能修改接收器值,而值接收器不行,问题的核心还是在 Golang 中的传参是传递值。
选择指针接收器还是值接收器?
在定义接收器为值还是指针时,主要有以下几个考虑点:
- 方法是否需要修改接收器本身,如果该方法需要改变接收器,则接收器必须是指针。
- 效率问题,如果接收器是值类型,那在方法调用时一定会产生值拷贝,而大对象拷贝代价很大。
- 一致性,对于同一个的方法,不应混用值接收器方法和指针接收器方法。
使用值接收器的场景:不需要编辑接收器的值,依赖值接收器的并发安全,出于效率的原因,例如小的不变结构或基本类型的值。
遇事不决,选择使用指针接收器。
最后的结论
除去 Golang 的语法糖,值类型不能调用指针接收器方法,指针类型既可以调用值接收器方法也可以调用指针接收器方法。