7.Go语言的接口
Go的接口是非侵入式的设计,也就是说,一个具体类型实现接口不需要在语法上显式地声明, 只要具体类型的方法集是接口方法的超集,就代表该类型实现了接口。
变量和实例
- "实例"里面蕴含着变量值、变量类型和附着在类型上的方法等语义。使用“实例”来代表具体类型的变量;
- 接口变量只有值和类型的概念,所以接口类型变量仍然称为接口变量,接口内部存放的具体类型变量被称为接口指向的“实例”.
空接口
最常使用的接口字面量类型就是空接口interface{}
。由于空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口,包括非命名类型的实例。
多态是指代码可以根据类型的具体实现采取不同行为的能力。如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。
接口值是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。这个内部表叫作 iTable,包含了所存储的值的类型信息。iTable 包含了已存储的值的类型信息以及与这个值相关联的一组方法。第二个字是一个指向所存储值的指针。将类型信息和指针组合在一起,就将这两个值组成了一种特殊的关系。
方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。
如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。为什么会有这种限制?事实上,编译器并不是总能自动获得一个值的地址。
1. 基本概念
1.1 接口声明
Go语言的接口分为接口字面量
和接口命名类型
,接口的声明使用interface
关键字。
- 接口字面量类型的声明语法如下:
interface {
MethodSignature1
MethodSignature2
}
- 接口命名类型使用type关键字声明:
type InterfaceName interface{
MethodSignature1
MethodSignature2
}
使用接口字面量的场景很少,一般只有空接口interface{}类型变量的声明才会使用。
- 接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是二者的混合。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
//如下三种声明是等价的,最终的展开模式都是第三种格式
type ReadWriter interface {
Reader
Writer
}
type ReadWriter interface {
Reader
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
方法声明
严格意义上的函数签名是函数的字面量类型,函数签名是不包括函数名的,而函数声明是指带上函数名的函数签名。
//方法声明=方法名+方法签名
MethodName (InputTypeList) OutputTypeList
Go语言在做接口匹配判断时是严格校验方法名称和方法签名的。
声明新接口类型的特点
(1)接口的命名一般以“er”结尾
(2)接口定义的内部方法声明不需要finc引导
(3)在接口定义中,只有方法声明没有方法实现
1.2 接口初始化
- 接口只有被初始化为具体的类型是才有意义。没有初始化的接口变量,其默认值是nil。
- 接口绑定具体类型的实例的过程称为接口初始化。接口变量支持两种直接初始化方法:
- 实例赋值接口:如果具体类型实例的方法是某个接口的方法集的超集,则称该具体类型实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会进行静态的类型检查。接口被初始化后,调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语义。
- 接口变量赋值接口变量:已经初始化的接口类型变量a直接赋值给另一种接口变量b,要求b的方法集是a的方法集的子集。
1.3 接口方法调用
- 接口方法调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法。接口方法调用不是一种直接的调用,有一定的运行时开销。
- 直接调用未初始化的接口变量的方法会产生panic。
package main
type Printer interface {
Print()
}
type S struct{}
func (s S) Print() {
println("print")
}
func main() {
var i Printer
//没有初始化的接口调用其方法会产生panic
//i.Print()
// 必须初始化
i = S{}
i.Print()
}
1.4 接口的动态类型和静态类型
动态类型
接口绑定的具体实例的类型称为接口的动态类型。接口可以绑定不同类型的实例,所以接口的动态类型是随着其绑定的不同类型实例而发生变化的。
静态类型
接口被定义时,其类型就已经被确定,这个类型叫接口的静态类型。接口的静态类型在其定义时就被确定,静态类型的本质特征就是接口的方法签名集合。
两个接口如何方法签名集合相同(顺序可以不同),则这两个接口在语义上完全等价,原因是Go编译器校验接口是否能赋值,是比较二者的方法集。
2. 接口运算
有时候需要知道已经初始化的接口变量绑定的具体实例是什么类型,以及这个具体实例是否还实现了其他接口。Go语言提供两种语法结构来支持这两种需求:分别是类型断言和接口类型查询。
2.1 类型断言(Type Assertion)
接口类型断言的语法如下:
i.(TypeName)
i必须是接口变量,如果是具体类型变量,则编译器会包non-interface type xxx on left。TypeName可以是接口类型名,也可以是具体类型名。
接口断言的两层含义:
(1)如果TypeName是一个具体类型名,则类型断言用于判断接口变量i绑定的实例类型是否就是具体类型TypeName。
(2)如果TypeName是一个接口类型名,则类型断言用于判断接口变量i绑定的实例类型是否同时实现了TypeName接口。
接口断言的两种语法表现
直接赋值模式:o := i.(TypeName)
package main
import "fmt"
type Inter interface {
Ping()
Pang()
}
type Anter interface {
Inter
String()
}
type St struct {
Name string
}
func (St) Ping() {
println("ping")
}
func (*St) Pang() {
println("pang")
}
func main() {
st := &St{"andes"}
var i interface{} = st
// 判断i绑定的实例是否实现了接口类型Inter
o := i.(Inter)
o.Ping()
o.Pang()
//如下语句会引发panic,因为i没有实现接口Anter
//p := i.(Anter)
//p.String()
//判断i绑定的实例是否就是具体类型St
s := i.(*St)
fmt.Printf("%s", s.Name)
}
comma,ok表达式模式如下:
if o, ok := i.(TypeName); ok {
}
代码改造之后:
func main() {
st := &St{"andes"}
var i interface{} = st
// 判断i绑定的实例是否实现了接口类型Inter
if o, ok := i.(Inter); ok {
o.Ping()
o.Pang()
}
if p, ok := i.(Anter); ok {
//i没有实现Anter接口,所以程序不会执行到这里
p.String()
}
//判断i绑定的实例是否就是具体类型St
if s, ok := i.(*St); ok {
fmt.Printf("%s", s.Name)
}
}
2.2 类型查询(Type Switches)
接口类型查询的语法格式如下:
switch v := i.(type) {
case type1:
xxxx
case type2:
xxxx
default:
xxxx
}
语义分析:
接口查询有两层语义,一是查询一个接口变量底层绑定的底层变量的具体类型是什么,二是查询接口变量绑定的底层变量是否还实现了其他接口。
(1)i必须是接口类型。如果i是未初始化接口变量,则v的值是nil。
(2)case字句后面可以跟非接口类型名,也可以跟接口类型名,匹配是按照case子句顺序进行的。
如果case后面是一个接口类型名,且接口变量i绑定的实例类型实现了该接口类型的方法,则匹配成功,v的类型是接口类型,v底层绑定的实例是i绑定具体实例的副本。
如果case后面是一个具体类型名,且接口变量i绑定的实例类型和该具体类型相同,则匹配成功,此时v就是该具体类型变量,v的值是i绑定的实例值的副本。
如果case后面跟着多个类型,使用逗号分隔,接口变量i绑定的实例类型只要和其中一个类型匹配,则直接使用o赋值给v,相当于v := o
如果所有的case字句都不满足,则执行default语句,此时执行的仍然是v := o
,最终v的值是o。此时使用v没有任何意义。
fallthrough语句不能在Type Switch语句中使用。
推荐switch的使用方式是将i.(type)赋值给一个新变量:
switch v:= i.(type) {
}
类型查询和类型断言
(1)类型查询和类型断言具有相同的语句,只是语法格式不同,二者都能判断接口变量绑定的实例的具体类型,以及判断接口变量绑定的实例是否满足另一个接口类型
(2)类型查询使用case字句一次判断多个类型,类型断言一次只能判断一个类型。
2.3 接口优点和使用形式
接口优点:
(1)解耦:Go的非侵入式的接口使层与层之间的代码更加干净,具体类型和实现的接口之间不需要显式声明,增加了接口使用的自由度。
(2)实现泛型:使用空接口作为函数或方法参数能够用在需要泛型的场景中。
接口使用形式:
(1)作为结构内嵌字段
(2)作为函数或方法的形参
(3)作为函数或方法的返回值
(4)作为其他接口定义的嵌入字段
3. 空接口
3.1 基本概念
没有任何方法的接口,称之为空接口。空接口表示为interface{}。Go语言的空接口内部封装了指针。
3.2 空接口的用途
空接口和泛型
Go语言没有泛型,如果一个函数需要接收任意类型的参数,则参数类型可以使用空接口类型。
空接口和反射
空接口是反射实现的基础,反射库就是将相关具体的类型转换并赋值给空接口后才去处理。
3.3 空接口和nil
空接口不是真的为空,接口有类型和值两个概念。示例如下:
package main
import "fmt"
type Inter interface {
Ping()
Pang()
}
type St struct {
}
func (St) Ping() {
println("ping")
}
func (*St) Pang() {
println("pang")
}
func main() {
var st *St = nil
var it Inter = st
fmt.Printf("%p\n", st)
fmt.Printf("%p\n", it)
if it != nil {
it.Pang()
下面的语句会导致Panic,方法转换为函数调用,第一个参数是St类型,由于*St是nil,无法获取指针所指的对象值,所以导致panic
//it.Ping()
}
}
//执行结果:
pang
0x0
0x0
空接口有两个字段,一个是实例类型,另一个是指向绑定实例的指针,只有两个都为nil时,空接口才为nil。
4. 接口内部实现
接口是Go语言类型系统的灵魂,是Go语言实现多态和反射的基础。Duck类型的接口完全解耦接口和具体实现者。
(1)接口的底层是如何实现的?
(2)如何实现动态调用?
(3)接口的动态调用到底有多大的额外开销?
4.1 数据结构
- 接口变量必须初始化才有意义,没有初始化的接口变量的默认值是nil,没有任何意义。
- 具体类型实例传递给接口称为接口的实例化。
非空接口的底层数据结构是iface,代码位于src/runtime/runtime2.go中
iface数据结构
非空接口初始化的过程就是初始化一个iface类型的结构,如下:
// src/runtime/runtime2.go
type iface struct {
tab *itab // itab存放类型及方法指针信息
data unsafe.Pointer //数据信息
}
可以看到iface结构很简单,有两个指针类型字段。
- itab:用来存放接口自身类型和绑定的实例类型及实例相关的函数指针。
- 数据指针data:指向接口绑定的实例的副本,接口的初始化也是一种值拷贝。
data指向具体的实例数据,如果传递给接口的是值类型,则data指向的是实例的副本,如果传递给接口的是指针类型,则data指向指针的副本。总而言之,无论接口的转换,还是函数的调用,Go遵循值传递
的规则。
itab数据结构,itab是接口内部实现的核心和基础,代码如下:
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptabs.
type itab struct {
inter *interfacetype // 接口自身的静态来行
_type *_type //_type就是接口存放的具体实例的类型(动态类型)
//hash存放具体类型的Hash值
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
itab有5个字段:
- inner是指向接口类型元信息的指针。
- _type是指向接口存放的具体类型元信息的指针,iface里的data指针指向的是该类型的值。一个是类型信息,另一个是类型的值。
- hash是具体类型的Hash值,_type里面也有hash,这里冗余存放主要是为了接口断言或类型查询时快速访问。
- fun是一个函数指针。虽然只有一个元素,实际上指针数组的大小是可变的,编译器负责填充,运行时使用底层指针进行访问,不会受struct类型越界检查的约束,这些指针指向的是具体类型的方法。
itab这个数据结构是非空接口实现动态调用的基础,itab的信息被编译器和链接器保存了下来,存放在可执行文件的只读存储段(.rodata)中。itab存放在静态分配的存储空间中,不受GC的限制,其内存不会被回收。
_type数据结构。Go语言是一种强类型的语言,编译器在编译时会做严格的类型校验,所以Go必然为每种类型维护一个类型的元信息,这个元信息在运行和反射时都会用到,Go语言的类型元信息的通用结构是_type,其他类型都是以_type为内嵌字段封装而成的结构体。
// src/runtime/type.go
// Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize,
// ../cmd/compile/internal/gc/reflect.go:/^func.dcommontype and
// ../reflect/type.go:/^type.rtype.
// ../internal/reflectlite/type.go:/^type.rtype.
type _type struct {
size uintptr //大小
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 //类型Hash
tflag tflag //类型的特征标记
align uint8 //_type作为整体变量存放时的对齐字节数
fieldAlign uint8 //当前结构字段的对齐字节数
kind uint8 //基础类型枚举值和反射中的Kind一致,kind决定了如何解析该类型
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte // GC相关信息
str nameOff //str用来表示类型名称字符串在编译后二进制文件中某个section的偏移量,由链接器负责填充
ptrToThis typeOff //ptrToThis用来表示类型元信息的指针在编译后二进制文件中某个section的偏移量,由链接器负责填充
}
- _type包含所有类型的共同元信息,编译器和运行时可以根据该元信息解析具体类型、类型名存放位置、类型的Hash值等基本信息。
- _type里面的nameOff和typeOff最终由链接器负责确定和填充,它们都是一个偏移量(offset),类型的名称和类型元信息实际上存放在连接后可执行文件的某个段(section)里,这两个值是相对于段内的偏移量,运行时提供两个转换查找函数。如下:
// 获取_type的name
func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {}
//获取_type的副本
func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {}
注意:
- Go语言类型元信息最初由编译器负责构建,并以表的形式存放在编译后的对象文件中,再由链接器在链接时进行段合并、符号重定向(填充某些值)。这些类型信息在接口的动态调用和反射中被运行时引用。
//接口方法元信息
type imethod struct {
name nameOff
ityp typeOff
}
//描述接口的类型
type interfacetype struct {
typ _type //类型通用部分
pkgpath name //接口所属包的名字信息,name内存放名称和描述信息
mhdr []imethod //接口的方法
}
4.2 接口调用过程分析
这里回顾一下之前学习的知识:Go函数使用的是caller-save
模式,即由调用者负责保存寄存器,所以在函数的头尾不会出现push ebp;move esp ebp这样的代码,相反其是在主调函数调用被调函数前后有一个保存现场和恢复现场的动作。
//开辟栈空间,压栈PB保存现场
SUBQ $X, SP //为函数开辟栈空间
MOVQ BP, y(SP) //保存当前函数BP到y(SP)位置,y为相对SP的偏移量
LEAQ y(SP), BP //重置BP,使其指向刚刚保存BP旧值的位置,这里主要是方便后续BP的恢复
//弹出栈, 恢复BP
MOVQ y(SP), BP//恢复BP的值为调用前的值
ADDQ $x, SP//恢复SP的值为函数开始时的值
几个抽象的寄存器如下:
- SB:Static base pointer,静态基址寄存器,它和全局符号一起表示全局变量的地址
- FP:Frame pointer,栈帧寄存器,该寄存器指向当前函数调用栈帧的栈底位置
- PC:Program counter,程序计数器,存放下一条指令的执行地址,很少直接操作该寄存器,一般时CALL、RET等指令隐式的操作
- SP:Stack pointer,栈顶寄存器,一般在函数调用前由主调函数设置SP的值对栈空间进行分配或回收
示例代码如下:
// iface.go
package main
type Caler interface {
Add(a, b int) int
Sub(a, b int) int
}
type Adder struct {
id int
}
func (adder Adder) Add(a, b int) int {
return a + b
}
func (adder Adder) Sub(a, b int) int {
return a - b
}
func main() {
var m Caler = Adder{id: 1234}
m.Add(10, 32)
}
利用如下命令生成汇编代码:
$ go build -gcflags="-S -N -l" iface.go >iface.s 2>&1
$ less iface.s
分析main函数的汇编代码:
这里是为main函数堆栈开辟空间并保存原来的BP指针。
FUNCDATA和垃圾回收可以忽略。
下面对于var m Caler = Adder{id: 1234}
的汇编:
在堆栈上初始化局部对象Adder,先初始化为0,后初始化为1234.
0x0046 00070 (/home/wechat/go/iface.go:19) LEAQ go.itab."".Adder,"".Caler(SB), CX
LEAQ指令是一个获取地址的指令,go.itab."".Adder,"".Caler(SB)
是一个全局符号引用,通过该符号能够获取前面介绍的接口初始化时itab数据结构的地址。注意,这个标号在链接器链接的过程中会替换为具体的地址,(SP)里面存放的是指向itab(Caler,Adder)的元信息的地址。
完整main函数的汇编如下:
"".main STEXT size=147 args=0x0 locals=0x48
0x0000 00000 (/home/wechat/go/iface.go:18) TEXT "".main(SB), ABIInternal, $72-0
0x0000 00000 (/home/wechat/go/iface.go:18) MOVQ (TLS), CX
0x0009 00009 (/home/wechat/go/iface.go:18) CMPQ SP, 16(CX)
0x000d 00013 (/home/wechat/go/iface.go:18) PCDATA $0, $-2
0x000d 00013 (/home/wechat/go/iface.go:18) JLS 137
0x000f 00015 (/home/wechat/go/iface.go:18) PCDATA $0, $-1
0x000f 00015 (/home/wechat/go/iface.go:18) SUBQ $72, SP
0x0013 00019 (/home/wechat/go/iface.go:18) MOVQ BP, 64(SP)
0x0018 00024 (/home/wechat/go/iface.go:18) LEAQ 64(SP), BP
0x001d 00029 (/home/wechat/go/iface.go:18) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (/home/wechat/go/iface.go:18) FUNCDATA $1, gclocals·54241e171da8af6ae173d69da0236748(SB)
0x001d 00029 (/home/wechat/go/iface.go:19) MOVQ $0, ""..autotmp_1+32(SP)
0x0026 00038 (/home/wechat/go/iface.go:19) MOVQ $1234, ""..autotmp_1+32(SP)
0x002f 00047 (/home/wechat/go/iface.go:19) MOVQ $1234, (SP)
0x0037 00055 (/home/wechat/go/iface.go:19) PCDATA $1, $0
0x0037 00055 (/home/wechat/go/iface.go:19) CALL runtime.convT64(SB)
0x003c 00060 (/home/wechat/go/iface.go:19) MOVQ 8(SP), AX
0x0041 00065 (/home/wechat/go/iface.go:19) MOVQ AX, ""..autotmp_2+40(SP)
0x0046 00070 (/home/wechat/go/iface.go:19) LEAQ go.itab."".Adder,"".Caler(SB), CX
0x004d 00077 (/home/wechat/go/iface.go:19) MOVQ CX, "".m+48(SP)
0x0052 00082 (/home/wechat/go/iface.go:19) MOVQ AX, "".m+56(SP)
0x0057 00087 (/home/wechat/go/iface.go:20) MOVQ "".m+48(SP), AX
0x005c 00092 (/home/wechat/go/iface.go:20) TESTB AL, (AX)
0x005e 00094 (/home/wechat/go/iface.go:20) MOVQ 24(AX), AX
0x0062 00098 (/home/wechat/go/iface.go:20) MOVQ "".m+56(SP), CX
0x0067 00103 (/home/wechat/go/iface.go:20) MOVQ CX, (SP)
0x006b 00107 (/home/wechat/go/iface.go:20) MOVQ $10, 8(SP)
0x0074 00116 (/home/wechat/go/iface.go:20) MOVQ $32, 16(SP)
0x007d 00125 (/home/wechat/go/iface.go:20) CALL AX
0x007f 00127 (/home/wechat/go/iface.go:21) MOVQ 64(SP), BP
0x0084 00132 (/home/wechat/go/iface.go:21) ADDQ $72, SP
0x0088 00136 (/home/wechat/go/iface.go:21) RET
0x0089 00137 (/home/wechat/go/iface.go:21) NOP
0x0089 00137 (/home/wechat/go/iface.go:18) PCDATA $1, $-1
0x0089 00137 (/home/wechat/go/iface.go:18) PCDATA $0, $-2
0x0089 00137 (/home/wechat/go/iface.go:18) CALL runtime.morestack_noctxt(SB)
0x008e 00142 (/home/wechat/go/iface.go:18) PCDATA $0, $-1
0x008e 00142 (/home/wechat/go/iface.go:18) JMP 0
0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 7a 48 dH..%....H;a.vzH
0x0010 83 ec 48 48 89 6c 24 40 48 8d 6c 24 40 48 c7 44 ..HH.l$@H.l$@H.D
0x0020 24 20 00 00 00 00 48 c7 44 24 20 d2 04 00 00 48 $ ....H.D$ ....H
0x0030 c7 04 24 d2 04 00 00 e8 00 00 00 00 48 8b 44 24 ..$.........H.D$
0x0040 08 48 89 44 24 28 48 8d 0d 00 00 00 00 48 89 4c .H.D$(H......H.L
0x0050 24 30 48 89 44 24 38 48 8b 44 24 30 84 00 48 8b $0H.D$8H.D$0..H.
0x0060 40 18 48 8b 4c 24 38 48 89 0c 24 48 c7 44 24 08 @.H.L$8H..$H.D$.
0x0070 0a 00 00 00 48 c7 44 24 10 20 00 00 00 ff d0 48 ....H.D$. .....H
0x0080 8b 6c 24 40 48 83 c4 48 c3 e8 00 00 00 00 e9 6d .l$@H..H.......m
0x0090 ff ff ff ...
rel 5+4 t=17 TLS+0
rel 56+4 t=8 runtime.convT64+0
rel 73+4 t=16 go.itab."".Adder,"".Caler+0
rel 125+0 t=11 +0
rel 138+4 t=8 runtime.morestack_noctxt+0
接口的动态调用分为两个阶段:
- 第一阶段就是构建iface动态数据结构,在接口实例化的时候完成;
- 第二阶段就是通过函数指针间接调用接口绑定的实例方法的过程。
使用readelf工具静态分析编译后的ELF格式的可执行程序:
可以看到符号表中go.itab.main.Adder,main.Caler
对应本程序里面itab的元信息,它被存放在第2段中,查看命令如下:
可以看到接口动态转换的数据元信息存放在.rodata段中。其他段的信息如下:
4.3接口调用代价
接口动态调用过程有两部分多余时耗:
- 一个是接口实例化的过程,即iface结构建立的过程,一旦实例化后,这个接口和具体类型的itab数据结构是可以复用的;
- 另一个是接口的方法调用,它是一个函数指针的间接调用。
同时接口调用是一种动态的计算后的跳转调用,会导致CPU缓存失效和分支预测失败。
测试代码如下:
package main
import (
"fmt"
"testing"
)
type identifier interface {
idInline() int32
idNoInline() int32
}
type id32 struct {
id int32
}
func (id *id32) idInline() int32 {
return id.id
}
// go:noinline
func (id *id32) idNoInline() int32 {
return id.id
}
var excapeMePlease *id32
//主要作用是强制变量内存在heap上分配
// go:noinline
func excapeToHeap(id *id32) identifier {
excapeMePlease = id
return excapeMePlease
}
//直接调用
func BenchmarkMethodCall_direct(b *testing.B) {
var myID int32
b.Run("single/noinline", func(b *testing.B) {
m := excapeToHeap(&id32{id: 6754}).(*id32)
b.ResetTimer()
for i := 0; i < b.N; i++ {
//CALL "".(*id32).idNoInline(SB)
//MOVL 8(SP), AX
//MOVQ "".&myID+40(SP), CX
//MOVL AX, (CX)
myID = m.idNoInline()
}
})
b.Run("single/inline", func(b *testing.B) {
m := excapeToHeap(&id32{id: 6754}).(*id32)
b.ResetTimer()
for i := 0; i < b.N; i++ {
//MOVL (DX), SI
//MOVL SI, (CX)
myID = m.idInline()
}
})
fmt.Println(myID)
}
//接口调用
func BenchmarkMethodCall_interface(b *testing.B) {
var myID int32
b.Run("single/noinline", func(b *testing.B) {
m := excapeToHeap(&id32{id: 6754})
b.ResetTimer()
for i := 0; i < b.N; i++ {
//MOVQ 32(AX), CX
//MOVQ "".m.data+40(SP), DX
//MOVQ DX, (SP)
//CALL CX
//MOVL 8(SP), AX
//MOVQ "".&myID+48(SP), CX
//MOVL AX, (CX)
myID = m.idNoInline()
}
})
b.Run("single/inline", func(b *testing.B) {
m := excapeToHeap(&id32{id: 6754})
b.ResetTimer()
for i := 0; i < b.N; i++ {
//MOVQ 24(AX),CX
//MOVQ "".m.data+40(SP),DX
//MOVQ DX, (SP)
//CALL CX
//MOVL 8(SP),AX
//MOVQ "".&myID+48(SP),CX
//MOVL AX, (CX)
myID = m.idInline()
}
})
fmt.Print(myID)
}
func main() {
}
测试过程和结果:
//直接调用
$ go test -bench='BenchmarkMethodCall_direct/single/noinline' -cpu=1 -count=5 iface_bench_test.go
//接口调用
$ go test -bench='BenchmarkMethodCall_interface/single/noinline' -cpu=1 -count=5 iface_bench_test.go
结果分析:
直接调用平均时耗为0.241ns/op,接口调用的平均时耗为1.748ns/op。(1.748-0.241)/0.241=6.253%。每次迭代接口要慢1.507ns,大约有6%的性能损失。
4.4 空接口数据结构
空接口interface{}是没有任何方法集的接口,所以空接口内部不需要维护和动态内存分配相关的数据结构itab。空接口只关心存放的具体类型是什么,具体类型的值是什么。底层数据结构如下:
type eface struct {
_type *_type
data unsafe.Pointer
}
从eface的数据结构上看,空接口不是真的为空,其保留了具体实例的类型和值拷贝,即便存放的具体类型是空的,空接口也不是空的。
空接口在Go语言中的真正意义是支持多态。有如下几种方式使用了空接口(将空接口类型还原):
(1)通过接口类型断言
(2)通过接口类型查询
(3)通过反射
至此,我们已经学习了Go语言中的接口以及相关的底层实现。下面我们将学习Go语言的并发。