本文基于go1.12.12源码进行分析,代码在amd64机器上运行和调试
(图片来源:百度百科)
图中的大黄鸭是一只鸭子吗?从传统角度来看,图中的大黄鸭并非是一只鸭子,因为它即不会叫也不会跑,甚至连生命都没有
首先看下鸭子类型的定义,摘自维基百科
If it walks like a duck and it quacks like a duck, then it must be a duck
如果某个东西像鸭子一样走,像鸭子一样嘎嘎叫,那它一定是鸭子
所以,从 Duck Typing
角度来看,图中的大黄鸭是一只鸭子
鸭子类型,是程序设计中的一种类型推断风格,它描述事物的外部行为而非内部结构
Go语言通过接口的方式实现Duck Typing
。不像其他动态语言那样,只能在运行时才能检查到类型不匹配,也不像大多数静态语言那样,需要显示声明实现哪个接口,Go语言接口的独特之处在于它是隐式实现
接口是一种抽象类型
,它没有暴露所含数据的布局或者内部结构,当然也没有哪些数据的基本操作,所提供的仅仅是一些方法。当你拿到一个接口类型的变量,你无从知道它是什么,但你能知道它能做什么,或者更精确地讲,仅仅是它提供了哪些方法
Go语言提供了 interface
关键字,接口中只能定义需要实现的方法,不能包含任何的变量
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
例如 io.Writer
类型,其实就是接口类型
type Writer interface {
Write(p []byte) (n int, err error)
}
接口与接口间可以嵌套得到新接口,如 io.ReadWriter
:
type Reader interface {
Read(p []byte) (n int, err error)
}
type ReadWriter interface{
Reader
Writer
}
不包含方法的接口,叫做空接口类型
interface{}
如果一个具体类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。当一表达式实现了一个接口时,这个表达式才可以复制给该接口
如下示例中,定义一个 Runner
接口,只包含一个 run()
方法, Person
结构体实现了 Run()
方法,那么就实现了 Runner
接口
type Runner interface {
Run()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func main() {
var r Runner
r = Person{Name: "song_chh"}
r.Run()
}
另外,因为空接口类型是没有定义任何方法的接口,因此所有类型都实现了空接口,也就是说可以把任何值赋给空接口类型
接口在定义一组方法时,并没有对实现的接收者做限制,所以会有两种实现方式,一种是指针接收者,另一种是值接收者
同一个方法不能两种实现同时存在
为Runner接口增加一个 Say()
方法,Person结构体类型使用指针接收者实现Say()方法
type Runner interface {
Run()
Say()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func (p *Person) Say() {
fmt.Printf("hello, %s", p.Name)
}
在对接口变量进行初始化时,可以使用结构体或者结构体指针
var r Runner
r = &Person{Name: "sch_chh"}
r = Person{Nmae: "sch_chh"}
因为实现接口的接受者类型和接口初始化时的类型都有两个维度,就会产生四种不同情况的编码
×
表示编译不通过
下面两种情况能够通过编译很好理解:
首先,我们来看一下能够通过编译的情况,也就是方法接收者是结构体,而初始化的变量是指针类型
type Runner interface {
Run()
Say()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func (p *Person) Say() {
fmt.Printf("hello, %s", p.Name)
}
func main() {
var r Runner
r = &Person{Name: "sch_chh"}
r.Run()
r.Say()
}
上述代码中,Person
结构体指针是能够直接调用Run
和Say
,因为作为结构体指针,能够隐式获取到底层的结构体,然后在通过结构体调用对应的方法
如果将引用去掉,即变量初始化使用结构体类型
r = Person{Name: "sch_chh"}
则会提示编译不通过
./pointer.go:24:4: cannot use Person literal (type Person) as type Runner in assignment:
Person does not implement Runner (Say method has pointer receiver)
那么为什么会编译不通过呢?首先在Go语言在进行参数传递都是 值传递
当代码中的变量是 &Person{}
时,在方法调用的过程中会对参数进行复制,创建一个新的 Person
结构体指针,指针指向一个确定的结构体,所以编译器会隐式的对变量解引用获取指针指向的结构体,完成方法的调用
当代码中的变量是 Person{}
时,在方法调用的过程中会对参数进行复制,也就是 Run()
和 Say()
会接受一个新的 Person{}
变量。如果方法接收者是 *Person
,编译器无法根据结构体找到一个唯一的指针,所以编译器会报错
注意:一个具体类型T的变量,直接调用*T的方法也是合法的,因为编译器会隐式的帮你完成取地址操作,但这仅仅是一个语法糖
再看一段示例,还是Runner接口和Person结构体,注意看main()函数体,首先声明一个接口变量r,打印是否为nil,紧接着定义一个*Person类型的p,打印p是否为nil,最后将p赋值给r,打印此时的r是否为nil
type Runner interface {
Run()
}
type Person struct {
Name string
}
func (p Person) Run() {
fmt.Printf("%s is running\n", p.Name)
}
func main() {
var r Runner
fmt.Println("r:", r == nil)
var p *Person
fmt.Println("p:", p == nil)
r = p
fmt.Println("r:", r == nil)
}
输出结果是什么?
r: true or false
p: true or false
r: true or false
实际输出结果为:
r: true
p: true
r: false
前两个输出r为nil和p为nil可以理解,因为接口类型和指针类型的零值为nil,那么当p赋值给r后,r却不为nil呢?其实是有个接口值的概念
从概念上来讲,一个接口类型的值(简称接口值)其实有两个部分:分别是 具体类型
和 该类型的值
,二者称为接口的动态类型
和动态值
,所以当且仅当接口的动态类型和动态值都为nil时,接口值才为nil
回到2.5的示例中,当p赋值给r接口后,r实际结构如图所示
验证一下是否真的是这样,在main函数体末尾加上一行代码
fmt.Printf("r type: %T, data: %v\n", r, r)
运行结果
r type: *main.Person, data: <nil>
可以看到动态值确实为nil
现在已经知道接口值的概念,那么接口底层实现具体是怎样的呢?
Go语言中的接口类型会根据是否包含一组方法
而分成两种不同的实现,分别为包含一组方法的iface
结构体和不包含任何方法的eface
结构体
iface底层是一个结构体,定义如下:
//runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
iface内部有两个指针,一个是itab结构体指针,另一个是指向数据的指针
unsafe.Pointer类型是一种特殊类型的指针,它可以存储任何变量的地址(类似C语言的void*)
//runtime/runtime2.go
type itab struct {
inter *interfacetype
_type *_type
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用于表示具体类型和接口类型关系,其中 inter
是接口类型定义信息,_type
是具体类型的信息,hash
是_type.hash的拷贝,在类型转换时,快速判断目标类型和接口中类型是否一致,fun
是实现方法地址列表,虽然fun固定长度为1的数组,但是这其实是一个柔型数组,保存元素的数量是不确定的,多个方法按照字典顺序排序
//runtime/type.go
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
```go
interfacetype是描述接口定义的信息,`_type`:接口的类型信息,`pkgpath`是定义接口的包名;,`mhdr`是接口中定义的函数表,按字典序排序
> 假设接口有ni个方法,实现接口的结构体有nt个方法,那么itab函数表生成时间复杂为O(ni*nt),如果接口方法列表和结构体方法列表有序,那么函数表生成时间复杂度为O(ni+nt)
```go
//runtime/type.go
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
// 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
str nameOff
ptrToThis typeOff
}
_type是所有类型的公共描述。size
是类型的大小,hash
是类型的哈希值;tflag
是类型的tags,与反射相关,align
和fieldalign
与内存对齐相关,kind
是类型编号,具体定义位于runtime/typekind.go中,gcdata
是gc相关信息
整个iface的结构图如下所示:
相对于iface,eface结构比较简单
//runtime/runtime2.go
type eface struct {
_type *_type
data unsafe.Pointer
}
eface内部同样有两个指针,一个具体类型信息_type结构体的指针,一个指向数据的指针
到此已经什么是接口,接口的底层结构,当具体类型赋值给接口类型时,是如何进行转换的?再来看下接口实现中的示例
1 package main
2
3 import "fmt"
4
5 type Runner interface {
6 Run()
7 }
8
9 type Person struct {
10 Name string
11 }
12
13 func (p Person) Run() {
14 fmt.Printf("%s is running\n", p.Name)
15 }
16
17 func main() {
18 var r Runner
19 r = Person{Name: "song_chh"}
20 r.Run()
21 }
通过Go提供的工具生成汇编代码
go tool compile -S interface.go
只截取与第19行相关的代码
0x001d 00029 (interface.go:19) PCDATA $2, $0
0x001d 00029 (interface.go:19) PCDATA $0, $1
0x001d 00029 (interface.go:19) XORPS X0, X0
0x0020 00032 (interface.go:19) MOVUPS X0, ""..autotmp_1+32(SP)
0x0025 00037 (interface.go:19) PCDATA $2, $1
0x0025 00037 (interface.go:19) LEAQ go.string."song_chh"(SB), AX
0x002c 00044 (interface.go:19) PCDATA $2, $0
0x002c 00044 (interface.go:19) MOVQ AX, ""..autotmp_1+32(SP)
0x0031 00049 (interface.go:19) MOVQ $8, ""..autotmp_1+40(SP)
0x003a 00058 (interface.go:19) PCDATA $2, $1
0x003a 00058 (interface.go:19) LEAQ go.itab."".Person,"".Runner(SB), AX
0x0041 00065 (interface.go:19) PCDATA $2, $0
0x0041 00065 (interface.go:19) MOVQ AX, (SP)
0x0045 00069 (interface.go:19) PCDATA $2, $1
0x0045 00069 (interface.go:19) PCDATA $0, $0
0x0045 00069 (interface.go:19) LEAQ ""..autotmp_1+32(SP), AX
0x004a 00074 (interface.go:19) PCDATA $2, $0
0x004a 00074 (interface.go:19) MOVQ AX, 8(SP)
0x004f 00079 (interface.go:19) CALL runtime.convT2I(SB)
0x0054 00084 (interface.go:19) MOVQ 16(SP), AX
0x0059 00089 (interface.go:19) PCDATA $2, $2
0x0059 00089 (interface.go:19) MOVQ 24(SP), CX
可以看到,编译器在构造itab后调用runtime.convT2I(SB)
转换函数,看下函数的实现
//runtime/iface.go
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
return
}
首先根据类型大小调用mallocgc
申请一块内存空间,将elem
指针的内容拷贝到新空间,将tab赋值给iface的tab,将新内存指针赋值给iface的data,这样一个iface就创建完成
将示例代码稍作更改,使用结构体指针类型的变量赋值给接口变量
19 r = &Person{Name: "song_chh"}
再次通过工具生成汇编代码
go tool compile -S interface.go
查看如下汇编代码
0x001d 00029 (interface.go:19) PCDATA $2, $1
0x001d 00029 (interface.go:19) PCDATA $0, $0
0x001d 00029 (interface.go:19) LEAQ type."".Person(SB), AX
0x0024 00036 (interface.go:19) PCDATA $2, $0
0x0024 00036 (interface.go:19) MOVQ AX, (SP)
0x0028 00040 (interface.go:19) CALL runtime.newobject(SB)
0x002d 00045 (interface.go:19) PCDATA $2, $2
0x002d 00045 (interface.go:19) MOVQ 8(SP), DI
0x0032 00050 (interface.go:19) MOVQ $8, 8(DI)
0x003a 00058 (interface.go:19) PCDATA $2, $-2
0x003a 00058 (interface.go:19) PCDATA $0, $-2
0x003a 00058 (interface.go:19) CMPL runtime.writeBarrier(SB), $0
0x0041 00065 (interface.go:19) JNE 105
0x0043 00067 (interface.go:19) LEAQ go.string."song_chh"(SB), AX
0x004a 00074 (interface.go:19) MOVQ AX, (DI)
首先编译器获取Person
结构体类型指针,作为参数调用runtime.newobject()
函数,同样的在源码中查看函数定义
// runtime/malloc.go
// implementation of new builtin
// compiler (both frontend and SSA backend) knows the signature
// of this function
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.size, typ, true)
}
newobject以*Person作为入参,创建新的Person
结构体指针,并将设置它的变量,之后由编译器生成iface
除了convT2I
函数外,其实在runtime/runtime.go
文件中,还有很多转换函数的定义
// Non-empty-interface to non-empty-interface conversion.
func convI2I(typ *byte, elem any) (ret any)
// Specialized type-to-interface conversion.
// These return only a data pointer.
func convT16(val any) unsafe.Pointer // val must be uint16-like (same size and alignment as a uint16)
func convT32(val any) unsafe.Pointer // val must be uint32-like (same size and alignment as a uint32)
func convT64(val any) unsafe.Pointer // val must be uint64-like (same size and alignment as a uint64 and contains no pointers)
func convTstring(val any) unsafe.Pointer // val must be a string
func convTslice(val any) unsafe.Pointer // val must be a slice
// Type to empty-interface conversion.
func convT2E(typ *byte, elem *any) (ret any)
func convT2Enoptr(typ *byte, elem *any) (ret any)
// Type to non-empty-interface conversion.
func convT2I(tab *byte, elem *any) (ret any) //for the general case
func convT2Inoptr(tab *byte, elem *any) (ret any) //for structs that do not contain pointers
convT2Inoptr
用于结构体内部不含指针的转换,noptr可以理解为no pointer,转换过程与convT2I
类似,
另外convT16
、convT32
、convT64
、convTstring
和 convTslice
是针对简单类型转接口的特例优化,查看源码convT64
,其余类似
//runtime/iface.go
func convT64(val uint64) (x unsafe.Pointer) {
if val == 0 {
x = unsafe.Pointer(&zeroVal[0])
} else {
x = mallocgc(8, uint64Type, false)
*(*uint64)(x) = val
}
return
}
相较于convT2
系列函数,缺少typedmemmove
和memmove
函数的调用,减少内存拷贝。另外如果值为该类型的零值,则不会调用 mallocgc
去申请一块新内存,直接返回指向zeroVal[0]
的指针
再来看下,空接口转换函数convT2E
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
}
if msanenabled {
msanread(elem, t.size)
}
x := mallocgc(t.size, t, true)
// TODO: We allocate a zeroed object only to overwrite it with actual data.
// Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.
typedmemmove(t, x, elem)
e._type = t
e.data = x
return
}
convT2E
和 convT2I
类似,同样在转换成eface时*_type
是由编译器生成,当做入参调用convT2E
上一节的内容主要介绍如何把具体类型转换成接口类型,那么怎样将具体类型转换成接口类型呢?Go语言提供两种方式,分别是类型断言
和类型分支
type assertion
类型断言有两种写法
v := x.(T)
v, ok := x.(T)
注意第一种写法,如果类型断言失败,会触发painc
type switch
switch x := x.(type) { /* ... */}
使用示例
switch i.(type) {
case string:
fmt.Println("i'm a string")
case int:
fmt.Println("i'm a int")
default:
fmt.Println("unknown")
}
【1】 《Go程序设计语言》机械工业出版社
【2】 《golang中interface底层分析》
【3】 《浅谈 Go 语言实现原理》
【4】 《深度解密Go语言之关于interface的10个问题》