4.2 接口
4.2.1 概述
在计算机科学中,接口是计算机系统中多个组件共享的边界,不同的组件能够在边界上交换信息。
接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。

例如:
- 可移植操作系统接口(Portable Operating System Interface,POSIX),定义了应用程序接口和命令行等标准,为计算机软件带来了可移植性 — 只要操作系统实现了 POSIX,计算机软件就可以直接在不同操作系统上运行。
 - SQL ,使用 SQL 语句查询数据时,无需关心底层数据库的具体实现,只在乎 SQL 返回的结果是否符合预期。
 
隐式接口
Golang 中的接口式隐式的,即无需声明要实现的接口,只需实现接口定义的方法即可。
类型
接口在Golang中是一种类型,有两类不同的接口:
runtime.iface, 带有方法的接口runtime.eface, 没有方法的接口,又称空接口,interface{}
注意:空接口不是任意类型,将其他类型赋值给空接口,将会进行隐式的类型转换。
指针和接口
在实现接口方法时,根据接收者不同会有两种:
- 接收者为值类型
 - 接收者为指针类型
 
当实现接口的类型的变量转换成接口类型时,在调用接口方法时会有不同的情况:
| 变量类型\接口方法的接收者类型 | 值 | 指针 | 
|---|---|---|
| 值类型 | 可 | 不可 | 
| 指针类型 | 可 | 可 | 
可以看出,当变量转换成接口类型后:
- 若变量是值类型,只能调用接受者为值类型的方法
 - 若变量是指针类型,则接受者为值类型和指针类型的方法均可调用
 

产生差异的原因是:Golang 中参数的传递是 值 传递;传参时只会复制参数的值。
- 当变量是指针类型,转换成接口类型后,接口变量中持有的是原变量的指针,在调用值类型的方法时,会自动解引用
 - 当变量是值类型,转换成接口类型后,接口变量中持有的是原变量的拷贝,在调用指针类型方法时,无法获取原变量的指针
 
nil 和 non-nil
空接口不是任意类型,可以通过下例来证明:
func NilOrNot(v interface{}) bool {
	return v == nil
}
func main() {
	var p *int
	fmt.Println(p == nil)
	fmt.Println(NilOrNot(p))
}
true
false
p是指针类型的零值,nil- 在将
p传给函数时,会进行隐式的类型转换,此时类型为空接口,不再和nil相等了 
4.2.2 数据结构
 空接口 和 _type结构体
空接口interface{}的数据结构为runtime.eface:
type eface struct {
	_type *_type
	data  unsafe.Pointer
}
_type:类型信息data:指向底层数据
从数据结构可以看出,任何变量都可以转换成空接口类型。
类型结构体runtime._type:
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:判断相同类型的变量是否相等
 接口 和 itab结构体
接口类型的数据结构为runtime.iface:
type iface struct {
	tab  *itab
	data unsafe.Pointer
}
其中runtime.itab:
type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32
	_     [4]byte
	fun   [1]uintptr 
}
inter:表示接口类型_type:表示具体类型hash:是_type.hash的拷贝,用于快速判断类型fun:是一个动态的数组,存储了一组函数指针。在使用时通过原始指针获取数据,所以元素数量是不确定的
4.2.3 类型转换
指针类型 -> 接口
以下列代码为例(使用//go:noinline禁止方法的内联编译):
package main
type Duck interface {
	Quack()
}
type Cat struct {
	Name string
}
//go:noinline
func (c *Cat) Quack() {
	println(c.Name + " meow")
}
func main() {
	var c Duck = &Cat{Name: "draven"}
	c.Quack()
}
将其编译成汇编,然后分析和赋值语句var c Duck = &Cat{Name: "draven"}相关的代码,可以分为三部分:
- 结构体
Cat的初始化 - 赋值触发的类型转换过程
 - 调用接口的方法 
Quack() 
结构体的初始化
LEAQ	type."".Cat(SB), AX                ;; AX = &type."".Cat
MOVQ	AX, (SP)                           ;; SP = &type."".Cat
CALL	runtime.newobject(SB)              ;; SP + 8 = &Cat{}
MOVQ	8(SP), DI                          ;; DI = &Cat{}
MOVQ	$6, 8(DI)                          ;; StringHeader(DI.Name).Len = 6
LEAQ	go.string."draven"(SB), AX         ;; AX = &"draven"
MOVQ	AX, (DI)                           ;; StringHeader(DI.Name).Data = &"draven"
- 获取
Cat类型的指针,将其放到栈上 - 调用
runtime.newobject在堆上为变量分配内存,并将其指针返回到SP+8上 - 将SP+8 存储到 DI 上
 - 为
Cat类型的变量分别赋值其字符串长度 6 和 字符串的值的指针(因为Cat只有一个string类型的字段) 

类型转换
LEAQ	go.itab.*"".Cat,"".Duck(SB), AX    ;; AX = *itab(go.itab.*"".Cat,"".Duck)
MOVQ	DI, (SP)      
接口类型的结构包含和类型相关的itab和指向原始数据的指针,此处将编译期生成的runtime.itab复制到SP上:

此时SP~SP+16就共同组成了runtime.iface结构体。
方法调用
CALL    "".(*Cat).Quack(SB)                ;; SP.Quack()
此处调用方法时,编译器进行了优化,将需要动态派发的方法调用改写成对目标方法的直接调用,以减少性能的额外开销。
若禁用编译器优化,就会看到动态派发的过程。
值类型 -> 接口
将示例代码的变量类型和方法都改为值类型:
package main
type Duck interface {
	Quack()
}
type Cat struct {
	Name string
}
//go:noinline
func (c Cat) Quack() {
	println(c.Name + " meow")
}
func main() {
	var c Duck = Cat{Name: "draven"}
	c.Quack()
}
关键的汇编代码依然分为三个部分:
- 初始化
Cat结构体 - 类型转换
 - 调用方法
 
初始化变量
XORPS   X0, X0                          ;; X0 = 0
MOVUPS  X0, ""..autotmp_1+32(SP)        ;; StringHeader(SP+32).Data = 0
LEAQ    go.string."draven"(SB), AX      ;; AX = &"draven"
MOVQ    AX, ""..autotmp_1+32(SP)        ;; StringHeader(SP+32).Data = AX
MOVQ    $6, ""..autotmp_1+40(SP)        ;; StringHeader(SP+32).Len = 6
此处在栈上初始化结构体变量Cat。
类型转换
LEAQ	go.itab."".Cat,"".Duck(SB), AX     ;; AX = &(go.itab."".Cat,"".Duck)
MOVQ	AX, (SP)                           ;; SP = AX
LEAQ	""..autotmp_1+32(SP), AX           ;; AX = &(SP+32) = &Cat{Name: "draven"}
MOVQ	AX, 8(SP)                          ;; SP + 8 = AX
CALL	runtime.convT2I(SB)      
此处调用 runtime.convT2I函数,以 go.itab."".Cat,"".Duck 的地址和指向 Cat 结构体的指针作为参数:
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
	t := tab._type
	x := mallocgc(t.size, t, true)
	typedmemmove(t, x, elem)
	i.tab = tab
	i.data = x
	return
}
函数根据类型大小在堆上分配内存并返回runtime.iface类型,其中包含runtime.itab指针和指向Cat类型的指针(在堆上新分配的Cat类型,不是原变量)。

此时, SP+32 位置的时原始的Cat类型变量,会将其拷贝到堆上(也就是发生了参数的拷贝)。
方法调用
MOVQ	16(SP), AX ;; AX = &(go.itab."".Cat,"".Duck)
MOVQ	24(SP), CX ;; CX = &Cat{Name: "draven"}
MOVQ	24(AX), AX ;; AX = AX.fun[0] = Cat.Quack
MOVQ	CX, (SP)   ;; SP = CX
CALL	AX         ;; CX.Quack()
MOVQ 24(AX), AX 从 runtime.itab 结构体中取出 Cat.Quack 方法指针作为 CALL 指令调用时的参数。
接口变量的第 24 字节是 itab.fun 数组开始的位置,由于 Duck 接口只包含一个方法,所以 itab.fun[0] 中存储的就是指向 Quack 方法的指针。
4.2.4 类型断言
将接口转换成具体类型,要用到类型断言。类型断言有两种方式:
v := varI.(T)v, ok := varI.(T),ok表示类型是否转换成功
接口
下例表示Duck 接口一个非空的接口,分析从 Duck 转换回 Cat 结构体的过程:
func main() {
	var c Duck = &Cat{Name: "draven"}
	switch c.(type) {
	case *Cat:
		cat := c.(*Cat)
		cat.Quack()
	}
}
将汇编指令分为两部分:
- 变量的初始化
 - 类型断言
 
变量初始化
00000 TEXT	"".main(SB), ABIInternal, $32-0
...
00029 XORPS	X0, X0
00032 MOVUPS	X0, ""..autotmp_4+8(SP)
00037 LEAQ	go.string."draven"(SB), AX
00044 MOVQ	AX, ""..autotmp_4+8(SP)
00049 MOVQ	$6, ""..autotmp_4+16(SP)
因为 编译器做了优化,代码中没有runtime.iface 的构建过程.
类型断言
00058 CMPL  go.itab.*"".Cat,"".Duck+16(SB), $593696792
                                        ;; if (c.tab.hash != 593696792) {
00068 JEQ   80                          ;;
00070 MOVQ  24(SP), BP                  ;;      BP = SP+24
00075 ADDQ  $32, SP                     ;;      SP += 32
00079 RET                               ;;      return
                                        ;; } else {
00080 LEAQ  ""..autotmp_4+8(SP), AX     ;;      AX = &Cat{Name: "draven"}
00085 MOVQ  AX, (SP)                    ;;      SP = AX
00089 CALL  "".(*Cat).Quack(SB)         ;;      SP.Quack()
00094 JMP   70                          ;;      ...
                                        ;;      BP = SP+24
                                        ;;      SP += 32
                                        ;;      return
                                        ;; }
switch语句生成的汇编指令会将目标类型的 hash 与接口变量中的 itab.hash 进行比较:
- 若相同,则表示接口变量的具体类型是
Cat:- 获取
Cat结构体指针 - 将指针拷贝到栈顶
 - 调用方法
 - 恢复函数栈,返回
 
 - 获取
 - 若不同,直接恢复栈顶指针并返回
 

空接口
func main() {
	var c interface{} = &Cat{Name: "draven"}
	switch c.(type) {
	case *Cat:
		cat := c.(*Cat)
		cat.Quack()
	}
}
如果不关闭 Go 语言编译器的优化选项,生成的汇编指令和接口的断言差不多,编译器会省略将 Cat 结构体转换成 runtime.eface 的过程。
如果禁用编译器优化,会在类型断言时不直接获取变量中具体类型的 runtime._type,而是从 eface._type 中获取,汇编指令仍然会使用目标类型的 hash 与变量的类型比较。
4.2.5 动态派发
动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程。
Go 接口的引入带来了动态派发特性,调用接口类型的方法时,如果编译期间不能确认接口的类型,会在运行期间决定具体调用该方法的哪个实现。
以下面的代码为例:
func main() {
	var c Duck = &Cat{Name: "draven"}
	c.Quack()
	c.(*Cat).Quack()
}
在关闭编译器的优化后,将其编译成汇编,在初始化过程之后栈上的数据如下:

- SP 是 Cat 类型,是运行时
runtime.newobject的参数 - SP+8 是 
runtime.newobject方法的返回值,即指向堆上的Cat结构体的指针 - P+32、SP+40 是对 SP+8 的拷贝,这两个指针都会指向堆上的 
Cat结构体 - SP+48 ~ SP+64 是接口变量 
runtime.iface结构体,其中包含了tab结构体指针和*Cat指针 
动态派发
c.Quack():
MOVQ	"".c+48(SP), AX                    ;; AX = iface(c).tab
MOVQ	24(AX), AX                         ;; AX = iface(c).tab.fun[0] = Cat.Quack
MOVQ	"".c+56(SP), CX                    ;; CX = iface(c).data
MOVQ	CX, (SP)                           ;; SP = CX = &Cat{...}
CALL	AX                                 ;; SP.Quack()
- 从接口变量中获取保存 
Cat.Quack方法指针的tab.func[0] - 接口变量在 
runtime.iface中的数据会被拷贝到栈顶 - 方法指针会被拷贝到寄存器中并通过汇编指令 
CALL触发 
类型转换后直接调用
c.(*Cat).Quack():
MOVQ	"".c+56(SP), AX                    ;; AX = iface(c).data = &Cat{...}
MOVQ	"".c+48(SP), CX                    ;; CX = iface(c).tab
LEAQ	go.itab.*"".Cat,"".Duck(SB), DX    ;; DX = &&go.itab.*"".Cat,"".Duck
CMPQ	CX, DX                             ;; CMP(CX, DX)
JEQ	163
JMP	201
MOVQ	AX, ""..autotmp_3+24(SP)           ;; SP+24 = &Cat{...}
MOVQ	AX, (SP)                           ;; SP = &Cat{...}
CALL	"".(*Cat).Quack(SB)                ;; SP.Quack()
在类型转换结束后,直接使用*Cat类型调用方法。
综上可以看出,动态派发会额外执行函数查找的过程。
Benchmark
下面两个方法 BenchmarkDirectCall 和 BenchmarkDynamicDispatch 分别会调用结构体方法和接口方法,在接口上调用方法时会使用动态派发机制,以直接调用作为基准分析动态派发带来了多少额外开销:
func BenchmarkDirectCall(b *testing.B) {
	c := &Cat{Name: "draven"}
	for n := 0; n < b.N; n++ {
		// MOVQ	AX, "".c+24(SP)
		// MOVQ	AX, (SP)
		// CALL	"".(*Cat).Quack(SB)
		c.Quack()
	}
}
func BenchmarkDynamicDispatch(b *testing.B) {
	c := Duck(&Cat{Name: "draven"})
	for n := 0; n < b.N; n++ {
		// MOVQ	"".d+56(SP), AX
		// MOVQ	24(AX), AX
		// MOVQ	"".d+64(SP), CX
		// MOVQ	CX, (SP)
		// CALL	AX
		c.Quack()
	}
}
使用 1 个 CPU 运行上述代码,每一个基准测试都会被执行 3 次:
$ go test -gcflags=-N -benchmem -test.count=3 -test.cpu=1 -test.benchtime=1s -bench=.
goos: darwin
goarch: amd64
pkg: github.com/golang/playground
BenchmarkDirectCall      	500000000	         3.11 ns/op	       0 B/op	       0 allocs/op
BenchmarkDirectCall      	500000000	         2.94 ns/op	       0 B/op	       0 allocs/op
BenchmarkDirectCall      	500000000	         3.04 ns/op	       0 B/op	       0 allocs/op
BenchmarkDynamicDispatch 	500000000	         3.40 ns/op	       0 B/op	       0 allocs/op
BenchmarkDynamicDispatch 	500000000	         3.79 ns/op	       0 B/op	       0 allocs/op
BenchmarkDynamicDispatch 	500000000	         3.55 ns/op	       0 B/op	       0 allocs/op
- 调用结构体方法时,每一次调用需要 ~3.03ns
 - 使用动态派发时,每一调用需要 ~3.58ns
 
动态派发生成的指令会带来 ~18% 左右的额外性能开销,但是在开启编译器优化后,动态派发的额外开销会降低至 ~5%,这对应用性能的整体影响就更小,所以与使用接口带来的好处相比,动态派发的额外开销往往可以忽略。
值类型和指针类型
func BenchmarkDirectCall(b *testing.B) {
	c := Cat{Name: "draven"}
	for n := 0; n < b.N; n++ {
		c.Quack()
	}
}
func BenchmarkDynamicDispatch(b *testing.B) {
	c := Duck(Cat{Name: "draven"})
	for n := 0; n < b.N; n++ {
		c.Quack()
	}
}
执行相同的基准测试时,会得到如下所示的结果:
$ go test -gcflags=-N -benchmem -test.count=3 -test.cpu=1 -test.benchtime=1s .
goos: darwin
goarch: amd64
pkg: github.com/golang/playground
BenchmarkDirectCall      	500000000	         3.15 ns/op	       0 B/op	       0 allocs/op
BenchmarkDirectCall      	500000000	         3.02 ns/op	       0 B/op	       0 allocs/op
BenchmarkDirectCall      	500000000	         3.09 ns/op	       0 B/op	       0 allocs/op
BenchmarkDynamicDispatch 	200000000	         6.92 ns/op	       0 B/op	       0 allocs/op
BenchmarkDynamicDispatch 	200000000	         6.91 ns/op	       0 B/op	       0 allocs/op
BenchmarkDynamicDispatch 	200000000	         7.10 ns/op	       0 B/op	       0 allocs/op
可以得出:
| 变量类型\调用方式 | 直接调用 | 动态派发 | 
|---|---|---|
| 指针 | ~3.03ns | ~3.58ns | 
| 结构体 | ~3.09ns | ~6.98ns | 
可以看到使用结构体实现接口带来的开销会大于使用指针实现,而动态派发在结构体上的表现非常差,所以应当尽量避免使用结构体类型实现接口。
使用结构体带来的巨大性能差异不只是接口带来的问题,带来性能问题主要因为 Go 语言在函数调用时是传值的,动态派发的过程只是放大了参数拷贝带来的影响。
