Go 语言里的接口赋值问题
Go 语言里的接口,先看定义:
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
其中 iface 描述接口变量(比如 var x IA)的内容及布局,变量大小是 16 个字节(64 位平台),两个字段分别是指向类似于函数表的指针、以及指向实际对象的指针;而 eface 描述 var y interface{} 这样的空接口,大小也是 16 个字节,两个字段分别是接口所指向的对象的类型,以及指向对象的指针。
熟悉这样的结构内容以及相应的含义,有助于对接口使用的理解,即使这些字段本身并不能在程序里直接使用。
以下是接口 “ 指向 ” 对象时的一个问题举例:
type Person struct {
age int
}
// Value Receiver
func (p Person) howOld() int {
return p.age
}
// 注意这里的 Pointer Receiver
func (p *Person) growUp() {
p.age += 1
}
type IPerson interface {
howOld() int
growUp()
}
然后定义一个对象,并用两种形式,通过接口 “ 指向 ” 它:
p0 := Person{18}
var ip IPerson
// (1)
//
// ip 指向 p0 对象的地址
// 这时,ip.data 里保存了 p0 的地址,即 ip.data = &p0
//
ip = &p0
ip.howOld()
ip.growUp()
// (2)
//
// ip 指向 p0 对象
// 实际上是生成了临时变量 tmp := p0;ip.data = &tmp
//
// 但这里有编译错误
//
ip = p0
在 (1) 处,可以编译过并执行。这里的接口变量 ip,像 “ 指针 ” 一样,作用了在 p0 上,即ip.data = &p0,运行时的行为和预期的一致。
在 (2) 处,会遇到编译错误。ip 指向 p0 对象,此处实际上是从 p0 复制并生成了一个临时变量 tmp,并让 ip.data 里保存了 tmp 的地址,即 tmp := p0; ip.data = &tmp。这是基于语法的理解。至于编译错误的原因,是因为 IPerson 接口里的 growUp() 方法,是实现在 pointer receiver 上。ip 指向 tmp 的地址后,如果可以编译过,那么 growUp() 只是作用在临时变量 tmp 上,实际修改的结果对外不可见,这就产生了歧义。为了避免这个隐藏的问题,编译器会对这样的使用提示错误。
所以这里需要注意区分接口实现上的 Value Receiver 和 Pointer Receiver 用法上的区别。以下是摘抄自手册里的说明:
Value Receiver
- The method receives a copy of the receiver's value.
- Modifications within the method do not affect the original value outside the method.
- Suitable for methods that only read the receiver's state or for immutable types.
Pointer Receiver
- The method receives a pointer to the receiver's value.
- Modifications within the method do affect the original value outside the method.
- Necessary for methods that need to modify the receiver's state or for large structs to avoid copying overhead.
接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。所以,当实现了一个接收者是值类型的方法,就可以自动(或者隐含地)生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。