Golang源码系列四:Interface实现原理分析(golang接口深入理解)

网友投稿 633 2022-06-14


本文基于go1.12.12源码进行分析,代码在amd64机器上运行和调试

1. Duck Typing

1.1 什么是鸭子类型

图中的大黄鸭是一只鸭子吗?如果从传统角度来看,图中的大黄鸭并非是一只鸭子,因为它即不会叫也不会跑,甚至连生命都没有

首先看下鸭子类型的定义

If it walks like a duck and it quacks like a duck, then it must be a duck

如果某个东西像鸭子一样走,像鸭子一样嘎嘎叫,那它一定是鸭子

所以,从Duck Typing角度来看,图中的大黄鸭是一只鸭子

鸭子类型,是程序设计中的一种类型推断风格,它描述事物的外部行为而非内部结构

1.2 Go语言的鸭子类型

Go语言通过接口的方式实现

Duck Typing

。不像其他动态语言那样,只能在运行时才能检查到类型不匹配,也不像大多数静态语言那样,需要显示声明实现哪个接口,Go语言接口的独特之处在于它是隐式实现

2. 概述

2.1 接口类型

接口是一种抽象类型,它没有暴露所含数据的布局或者内部结构,当然也没有哪些数据的基本操作,所提供的仅仅是一些方法。当你拿到一个接口类型的变量,你无从知道它是什么,但你能知道它能做什么,或者更精确地讲,仅仅是它提供了哪些方法。

2.2 接口定义

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{}

2.3 实现接口

如果一个具体类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。当具体类型实现了一个接口时,这个具体类型才可以赋值给该接口

如下示例中,定义一个

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()}

另外,因为空接口类型是没有定义任何方法的接口,因此所有类型都实现了空接口,也就是说可以把任何类型赋给空接口类型

2.4 接口和指针

接口在定义一组方法时,没有对实现的接收者做限制,所以有两种实现方式的接收者,一种是指针接收者,另一种是值接收者

同一个方法不能两种实现同时存在

为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 Runnerr = &Person{Name: "song_chh"}r = Person{Name: "song_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: "song_chh"} r.Run() r.Say()}

上述代码中,Person结构体指针是能够直接调用Run和Say,因为作为结构体指针,能够隐式获取到底层的结构体,然后在通过结构体调用对应的方法

如果将引用去掉,即变量初始化使用结构体类型

r = Person{Name: "song_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的方法也是合法的,因为编译器会隐式的帮你完成取地址操作,但这仅仅是一个语法糖

2.5 nil和non-nil

再看一段示例,还是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 falsep: true or falser: true or false

实际输出结果为:

r: truep: truer: false

前两个输出r为nil和p为nil,因为接口类型和指针类型的零值为nil,那么当p赋值给r后,r却不为nil呢?其实是有个接口值的概念

2.6 接口值

从概念上来讲,一个接口类型的值(简称接口值)其实有两个部分:分别是

具体类型

该类型的值

,二者称为接口的

动态类型

动态值

,所以当且仅当接口的动态类型和动态值都为nil时,接口值才为nil

回到2.5的示例中,当p赋值给r接口后,r实际结构如图所示

验证一下是否真的是这样,在main函数体末尾加上一行代码

fmt.Printf("r type: %T, data: %v\n", r, r)

运行结果

r type: *main.Person, data:

可以看到动态值确实为nil

现在已经知道接口值的概念,那么接口底层实现具体是怎样的呢?

3. 实现原理

Go语言中的接口类型会根据是否包含一组方法而分成两种不同的实现,分别为包含一组方法的iface结构体和不包含任何方法的eface结构体

3.1 iface

iface底层是一个结构体,定义如下:

//runtime/runtime2.gotype iface struct { tab *itab data unsafe.Pointer}

iface内部有两个指针,一个是itab结构体指针,另一个是指向数据的指针

unsafe.Pointer类型是一种特殊类型的指针,它可以存储任何变量的地址(类似C语言的void*)

//runtime/runtime2.gotype 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.gotype interfacetype struct { typ _type pkgpath name mhdr []imethod}

interfacetype是描述接口定义的信息,_type:接口的类型信息,pkgpath是定义接口的包名;,mhdr是接口中定义的函数表,按字典序排序

假设接口有ni个方法,实现接口的结构体有nt个方法,正常情况itab函数表生成时间复杂为O(ni*nt),如果接口方法列表和结构体方法列表有序,那么函数表生成时间复杂度为O(ni+nt)

//runtime/type.gotype _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的结构图如下所示:

3.2 eface

相对于iface,eface结构比较简单

//runtime/runtime2.gotype eface struct { _type *_type data unsafe.Pointer}

eface内部同样有两个指针,一个具体类型信息_type结构体的指针,一个指向数据的指针

3.3 具体类型转换成接口类型

到此已经知道什么是接口以及接口的底层结构,那么当具体类型赋值给接口类型时,是如何进行转换的?再来看下2.3中的示例

package main import "fmt" 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()}

通过Go提供的工具生成汇编代码

go tool compile -S interface.go

只截取与第19行相关的代码

0x001d 00029 (interface.go:19) PCDATA $2, $00x001d 00029 (interface.go:19) PCDATA $0, $10x001d 00029 (interface.go:19) XORPS X0, X00x0020 00032 (interface.go:19) MOVUPS X0, ""..autotmp_1+32(SP)0x0025 00037 (interface.go:19) PCDATA $2, $10x0025 00037 (interface.go:19) LEAQ go.string."song_chh"(SB), AX0x002c 00044 (interface.go:19) PCDATA $2, $00x002c 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, $10x003a 00058 (interface.go:19) LEAQ go.itab."".Person,"".Runner(SB), AX0x0041 00065 (interface.go:19) PCDATA $2, $00x0041 00065 (interface.go:19) MOVQ AX, (SP)0x0045 00069 (interface.go:19) PCDATA $2, $10x0045 00069 (interface.go:19) PCDATA $0, $00x0045 00069 (interface.go:19) LEAQ ""..autotmp_1+32(SP), AX0x004a 00074 (interface.go:19) PCDATA $2, $00x004a 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), AX0x0059 00089 (interface.go:19) PCDATA $2, $20x0059 00089 (interface.go:19) MOVQ 24(SP), CX

可以看到,编译器在构造itab后调用

runtime.convT2I(SB)

转换函数,看下函数的实现

//runtime/iface.gofunc 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就创建完成

将示例代码稍作更改,使用结构体指针类型的变量赋值给接口变量

r = &Person{Name: "song_chh"}

再次通过工具生成汇编代码

go tool compile -S interface.go

查看如下汇编代码

0x001d 00029 (interface.go:19) PCDATA $2, $10x001d 00029 (interface.go:19) PCDATA $0, $00x001d 00029 (interface.go:19) LEAQ type."".Person(SB), AX0x0024 00036 (interface.go:19) PCDATA $2, $00x0024 00036 (interface.go:19) MOVQ AX, (SP)0x0028 00040 (interface.go:19) CALL runtime.newobject(SB)0x002d 00045 (interface.go:19) PCDATA $2, $20x002d 00045 (interface.go:19) MOVQ 8(SP), DI0x0032 00050 (interface.go:19) MOVQ $8, 8(DI)0x003a 00058 (interface.go:19) PCDATA $2, $-20x003a 00058 (interface.go:19) PCDATA $0, $-20x003a 00058 (interface.go:19) CMPL runtime.writeBarrier(SB), $00x0041 00065 (interface.go:19) JNE 1050x0043 00067 (interface.go:19) LEAQ go.string."song_chh"(SB), AX0x004a 00074 (interface.go:19) MOVQ AX, (DI)

首先编译器通过

type."".Person(SB)

获取

Person

结构体类型,作为参数调用

runtime.newobject()

函数,同样的在源码中查看函数定义

import "unsafe" // runtime/malloc.go // implementation of new builtin// compiler (both frontend and SSA backend) knows the signature// of this functionfunc 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 stringfunc 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 casefunc convT2Inoptr(tab *byte, elem *any) (ret any) //for structs that do not contain pointers

convI2I

用于接口转换成另一个接口时调用,在3.4会进行讲解

convT2Inoptr

用于变量内部不含指针的转换,noptr可以理解为no pointer,转换过程与

convT2I

类似

convT16

convT32

convT64

convTstring

convTslice

是针对简单类型转接口的特例优化,有兴趣的可以看下函数实现的源码,因为这几个函数内容相似,这里就简单介绍下

convT64

//runtime/iface.gofunc 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

3.4 接口与接口的转换

如果某个类型实现多个接口,那接口直接是如何进行转换的,还是先看一段示

package main import "fmt" type Runner interface { Run() Say()} type Sayer interface { 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: "song_chh"} var s Sayer s = r s.Say()}

增加

Sayer

接口定义,包含

Say()

方法,在main函数中声明一个

Sayer

变量,并将

Runner

接口变量赋值给

Sayer

变量。因为

Person

实现了

Say()

方法,所以说

Person

既实现了是

Runner

接口,又实现了

Sayer

接口

执行命令

go tool compile -S interface.go

截取32行汇编代码

0x0062 00098 (interface.go:32) PCDATA $2, $30x0062 00098 (interface.go:32) LEAQ type."".Sayer(SB), DX0x0069 00105 (interface.go:32) PCDATA $2, $20x0069 00105 (interface.go:32) MOVQ DX, (SP) 0x006d 00109 (interface.go:32) MOVQ AX, 8(SP)0x0072 00114 (interface.go:32) PCDATA $2, $00x0072 00114 (interface.go:32) MOVQ CX, 16(SP)0x0077 00119 (interface.go:32) CALL runtime.convI2I(SB)0x007c 00124 (interface.go:32) MOVQ 24(SP), AX0x0081 00129 (interface.go:32) PCDATA $2, $20x0081 00129 (interface.go:32) MOVQ 32(SP), CX

可以看到在执行期间,调用

runtime.convI2I

进行接口转换,接下来看下源代码

func convI2I(inter *interfacetype, i iface) (r iface) { tab := i.tab if tab == nil { return } if tab.inter == inter { r.tab = tab r.data = i.data return } r.tab = (inter, tab._type, false) r.data = i.data return}

函数参数

inter

表示接口的类型,由编译器生成,即

type."".Sayer(SB)

i

是绑定实体的接口,

r

是转换后新的接口,如果要转换的接口是同一类型,则直接把

i

的tab和data给新接口

r

,将

r

返回。如果要转换的接口不是同一类型,则通过

getitab

生成一个新的tab复制给

r.tab

,然后将

r

返回

那么具体来看一下

getitab

这个函数,还是先看源码

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab { if len(inter.mhdr) == 0 { throw("internal error - misuse of itab") } // easy case if typ.tflag&tflagUncommon == 0 { if canfail { return nil } name := inter.typ.nameOff(inter.mhdr[0].name) panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()}) } var m *itab // First, look in the existing table to see if we can find the itab we need. // This is by far the most common case, so do it without locks. // Use atomic to ensure we see any previous writes done by the thread // that updates the itabTable field (with atomic.Storep in itabAdd). t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable))) if m = t.find(inter, typ); m != nil { goto finish } // Not found. Grab the lock and try again. lock(&itabLock) if m = itabTable.find(inter, typ); m != nil { unlock(&itabLock) goto finish } // Entry doesn't exist yet. Make a new entry & add it. m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys)) m.inter = inter m._type = typ m.init() itabAdd(m) unlock(&itabLock)finish: if m.fun[0] != 0 { return m } if canfail { return nil } // this can only happen if the conversion // was already done once using the , ok form // and we have a cached negative result. // The cached result doesn't record which // interface function was missing, so initialize // the itab again to get the missing function name. panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})}

首先,用

t

保存全局

itabTable

的地址,使用

t.find

进行查找,这个是通用的查找

如果没有查找到,就会上锁,重新使用

itabTable.find

进行查找

再没有找到,就会根据具体类型

typ

和接口类型

inter

生成一个

itab

,并将这个新生成的

itab

添加到全局的

itabTable

中。如果具体类型并没有实现接口,根据

canfail

返回

nil

或者

painc

3.5 断言

上一节的内容主要介绍如何把具体类型转换成接口类型,那么怎样将接口类型转换成具体类型呢?Go语言提供两种方式,分别是类型断言和类型分支

type assertion

类型断言有两种写法

v := x.(T)v, ok := x.(T)

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")}

接口转具体类型时,是由编译器进行对比,进行转换的,并非是在运行时动态调用某个函数


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:“网红” WebAssembly 与 K8s 如何实现双剑合璧?(网红小吃)
下一篇:Golang源码系列三:Channel实现原理分析(go channel实现原理)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~