gocookbook icon indicating copy to clipboard operation
gocookbook copied to clipboard

接口的类型和方法的接收者

Open kevinyan815 opened this issue 4 years ago • 0 comments

注:所有内容摘录自 Go 语言接口的实现原理,为该文章的读书笔记。

接口类型

接口也是 Go 语言中的一种类型,它能够出现在变量的定义、函数的入参和返回值的类型定义部分它们进行类型约束,不过 Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的空接口 interface{}。

Go 语言使用 runtime.iface 表示包含方法签名的接口,使用 runtime.eface 表示不包含任何方法的空接口 interface{}。

type iface struct {
	tab  *itab
	data unsafe.Pointer
}

与 C 语言中的 void * 不同,interface{} 类型不是任意类型。如果我们将类型转换成了 interface{} 类型,变量在运行期间的类型也会发生变化,获取变量类型时得到的类型是 interface{}。

runtime.eface 类型在 Go 语言中的定义如下:

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

interface{} 类型不包含任何方法,所以它对应的结构也相对来说比较简单,只包含指向底层数据和类型的两个指针。从上述结构我们也能推断出 — Go 语言的任意类型都可以转换成 interface{}。

runtime.iface 这个结构体类型中有指向原始数据的指针 data,不过更重要的是 runtime.itab 类型的 tab 字段。

type iface struct { // 16 字节
	tab  *itab
	data unsafe.Pointer
}

接下来我们将详细分析 Go 语言接口中的这两个类型,即 runtime._typeruntime.itab

运行时类型表示

runtime._type 是 Go 语言类型的运行时表示。下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等。

type _type struct {
	size       uintptr
	ptrdata    uintptr
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal      func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata     *byte
	str        nameOff
	ptrToThis  typeOff
}
  • size 字段存储了类型占用的内存空间,为内存空间的分配提供信息;
  • hash 字段能够帮助我们快速确定类型是否相等;
  • equal 字段用于判断当前类型的多个对象是否相等

runtime.itab 结构体是接口类型的核心组成部分,每一个 runtime.itab 都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter 和 _type 两个字段表示:

type itab struct { // 32 字节
	inter *interfacetype
	_type *_type
	hash  uint32
	_     [4]byte
	fun   [1]uintptr
}

除了 inter 和 _type 两个用于表示类型的字段之外,上述结构体中的另外两个字段也有自己的作用:

  • hash 是对 _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断接口类型的这个字段和具体类型 runtime._type 的 hash 是否一致
  • fun 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的;

我们会在类型断言中介绍 hash 字段的使用,在动态派发一节中介绍 fun 数组中存储的函数指针是如何被使用的。

实现接口的接收者

接口在定义一组方法时没有对实现方法的接收者做限制,所以经常会看到由某个类型或者某个类型的指针实现接口方法这两种形式。比如下面这样:

type Cat struct {}
type Duck interface { ... }

func (c  Cat) Quack {}  // 使用结构体实现接口
func (c *Cat) Quack {}  // 使用结构体指针实现接口

虽然Cat*Cat是两种类型,都可以实现Duck接口,但是上图中的两种实现不可以同时存在,Go 语言的编译器会在结构体类型和类型的指针都实现同一个方法时报错 “method redeclared”

当使用结构体Cat实现接口Duck时,通过结构体指针*Cat调用接口的方法是允许的

type Cat struct{}

func (c Cat) Quack() {
	fmt.Println("meow")
}

func main() {
	var c Duck = &Cat{}
	c.Quack()
}

针对上面这种情况编译器在编译时会自动做解引用通过指针找到指向的结构体再去调用方法。

但是如果我们将上述代码中方法的接受者和初始化的类型进行交换,代码就无法通过编译了:

type Duck interface {
	Quack()
}

type Cat struct{}

func (c *Cat) Quack() {
	fmt.Println("meow")
}

func main() {
	var c Duck = Cat{}
	c.Quack()
}

$ go build interface.go
./interface.go:20:6: cannot use Cat literal (type Cat) as type Duck in assignment:
	Cat does not implement Duck (Quack method has pointer receiver)

编译器会提醒我们:Cat 类型没有实现 Duck 接口,Quack 方法的接受者是指针。这两个报错对于刚刚接触 Go 语言的开发者比较难以理解,如果我们想要搞清楚这个问题,首先要知道 Go 语言在传递参数时都是传值的。

无论上述代码中初始化的变量 c 是 Cat{} 还是 &Cat{},使用 c.Quack() 调用方法时都会发生值拷贝:

  • 如果是结构体类型实现了接口,这意味着用 &Cat 指针调用接口方法时会拷贝一个新的 &Cat 指针,这个指针与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用(dereference)获取指针指向的结构体

  • 如果是指针类型实现的接口,对于 Cat{} 来说,这意味着 Quack 方法会接受一个全新的 Cat{},但是因为接口实现方法的接收者是 *Cat,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,因为值拷贝,这个指针指向的也不是最初调用该方法的结构体;

上面的分析解释了指针类型的现象,当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口。

kevinyan815 avatar Jan 12 '21 09:01 kevinyan815