深度解密Go语言之关于 interface 的10个问题(二)
值接收者和指针接收者
前面说过,不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。
先说结论:实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。
来看一个例子,就会完全明白:
package main
import "fmt"
type coder interface {
code()
debug()
}
type Gopher struct {
language string
}
func (p Gopher) code() {
fmt.Printf("I am coding %s language\n", p.language)
}
func (p *Gopher) debug() {
fmt.Printf("I am debuging %s language\n", p.language)
}
func main() {
var c coder = &Gopher{"Go"}
c.code()
c.debug()
}
上述代码里定义了一个接口 coder ,接口定义了两个函数:
code()
debug()
接着定义了一个结构体 Gopher ,它实现了两个方法,一个值接收者,一个指针接收者。
最后,我们在 main 函数里通过接口类型的变量调用了定义的两个函数。
运行一下,结果:
I am coding Go language
I am debuging Go language
但是如果我们把 main 函数的第一条语句换一下:
func main() {
var c coder = Gopher{"Go"}
c.code()
c.debug()
}
运行一下,报错:
./main.go:23:6: cannot use Gopher literal (type Gopher) as type coder in assignment:
Gopher does not implement coder (debug method has pointer receiver)
看出这两处代码的差别了吗?第一次是将 &Gopher 赋给了 coder ;第二次则是将 Gopher 赋给了 coder 。
第二次报错是说,Gopher 没有实现 coder 。很明显了吧,因为 Gopher 类型并没有实现 debug 方法;表面上看, *Gopher 类型也没有实现 code 方法,但是因为 Gopher 类型实现了 code 方法,所以让 *Gopher 类型自动拥有了 code 方法。
当然,上面的说法有一个简单的解释:接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。
所以,当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。
最后,只要记住下面这点就可以了:
如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。
两者分别在何时使用
如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
使用指针作为方法的接收者的理由:
- 方法能够修改接收者指向的值。
- 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质 。
如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header , 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header ,而 header 本身就是为复制设计的。
如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体 。
这一段说的比较绕,大家可以去看《Go 语言实战》5.3 那一节。
3. iface 和 eface 的区别是什么
iface 和 eface 都是 Go 中描述接口的底层结构体,区别在于 iface 描述的接口包含方法,而 eface 则是不包含任何方法的空接口:interface{} 。
从源码层面看一下:
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
iface 内部维护两个指针,tab 指向一个 itab 实体, 它表示接口的类型以及赋给这个接口的实体类型。data 则指向接口具体的值,一般而言是一个指向堆内存的指针。
再来仔细看一下 itab 结构体:_type 字段描述了实体的类型,包括内存对齐方式,大小等;inter 字段则描述了接口的类型。fun 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。
这里只会列出实体类型和接口相关的方法,实体类型的其他方法并不会出现在这里。如果你学过 C++ 的话,这里可以类比虚函数的概念。
另外,你可能会觉得奇怪,为什么 fun 数组的大小为 1,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的。
再看一下 interfacetype 类型,它描述的是接口的类型:
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
可以看到,它包装了 _type 类型,_type 实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 mhdr 字段,表示接口所定义的函数列表, pkgpath 记录定义了接口的包名。
这里通过一张图来看下 iface 结构体的全貌:
接着来看一下 eface 的源码: |