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 ReceiverPointer 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.

接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。所以,当实现了一个接收者是值类型的方法,就可以自动(或者隐含地)生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。

Read More: