1. 介绍
本文介绍的是1.17.10版本的interface相关的源码实现部分。
不同于int, struct等具体类型,接口类型是一种抽象类型,它只是定义了规则,是一种约定。通过接口类型的变量或对象,仅仅知道其能提供哪些方法而已。
本文从源码角度去理解interface如何是一种约定的。
2. 非空interface类型
非空接口即是我们常见的定义了一些函数签名的,这些函数就是规则,约定了实现该接口的具体类型需要提供的方法集。
下面代码中的IPeople即是非空接口类型,而People因为有GetName 和 GetAge这两个函数,即是说People实现了IPeople。
package main
import "fmt"
type IPeople interface {
GetName() string
GetAge() int
}
type People struct {
name string
age int
}
func (p People) GetName() string {
return p.name
}
func (p People) GetAge() int {
return p.age
}
func (p People) GetHeight() int {
return 170
}
func test(p IPeople) {
fmt.Printf("Name: %s, Age: %d \n", p.GetName(), p.GetAge())
}
func main() {
var p People = People{name: "timefly32", age: 18}
test(p) // 会先调用runtime.convT2I(SB),把People转为IPeople
}
下面是非空interface数据结构相关的源码,对应上面的应用代码,加了注释。
type iface struct {
tab *itab
data unsafe.Pointer //指向具体类型People的数据部分
}
//这个struc很重要,包含接口类型和具体类型的"元数据"
type itab struct {
inter *interfacetype //接口类型相关,即IPeople相关部分发
_type *_type //具体类型的类型,即People的_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr //具体类型People实现的方法集,即实现的约定, variable sized. fun[0]==0 means _type does not implement inter.
}
type interfacetype struct {
typ _type //IPeople的_type
pkgpath name
mhdr []imethod //IPeople定义的函数集,即约定
}
一图胜千言:
通过上面的代码和图片可以看到,interface在源码角度上,也是个struct,只不过能因为它是一种约定,一种抽象,所以源码设计上,作者通过iface这个结构来表示非空interface类型的对象,itab(很重要)把原interface类型(IPeople)和具体类型(IPeople)的"元数据"都附带上了,其中包含了原interface类型定义的函数集合(约定),和具体类型实现的函数集合(实现了约定),即约定和实现了约定。当然,在编译或者runtime创建iface/itab的时候,会根据interface定义函数规则,查找具体类型的方法集,当任何一个函数没有实现的话,约定即未实现,转换失败。
a. 具体类型 转 interface
上面的测试代码中,main函数中的p是个People具体类型变量,而test函数的参数是个IPeople,所以golang的runtime在调用test函数前,调用了convT2I,做了转换,即具体类型转interface类型。
convT2I的源码如下,其实在进入这个函数之前,itab这变量已经是完整的了,里面包含了interface的元数据和具体类型的元数据,这个应该是编译或runtime完成的。这个convT2I其实是用来复制具体类型的数据部分。
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
}
下图是通过gdb查看tab和elem的数据后更新的:
gdb查看的细节部分,可略过不看:
itab中的fun其实是个二维指针,指向一个数组,数组的元素是函数指针,里面是具体类型People实现IPeople约定的所有函数,GetAge和GetName, 按照函数名排序的。
elem指向的是具体类型的数据,即 name 和 age,最后复制给了iface中的data。
type People struct {
name string
age int
}
People{name: "timefly32", age: 18}
//string底层是用stringStruct表示的,一个指针指向字符串地址,一个是len
type stringStruct struct {
str unsafe.Pointer
len int
}
b. interface 转 interface
非空interface变量,由上面的分析可以简单的理解,它的底层的数据结的iface的如下图:
这样test函数中的interface变量p再次转换为其他interface的时候,是可以完整的拿到原始的People的type, 方法和数据等,原则上只要People能转成的interface类型,此时p也能转成功。但是编译器会检查,interface中的函数是否是目标interface的函数集全集,否则编译报错。
- 源interface中的函数是全集
先看源interface中的函数是目标interface的全集的情况。
type IPeople interface {
GetName() string
GetAge() int
}
type IPeople1 interface {
GetName() string
}
func test1(p IPeople1) {
fmt.Printf("Name: %s \n", p.GetName())
}
func test(p IPeople) {
test1(p) // ok
fmt.Printf("Name: %s, Age: %d \n", p.GetName(), p.GetAge())
}
上面新增了IPeople1 interface, 它只有一个函数GetName,调用test1(p)时,底层调用的是runtime.convI2I,
//i是源interface, inter是目标interface
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 = getitab(inter, tab._type, false) //这个函数很重要,获取/创建初始化一个itab
r.data = i.data
return
}
getitab函数就是想获取/创建一个itab,这个函数很重要,将来会在讲类型断言assert的时候重点讲解,
// inter是目标interface的inter
// typ 是源interface附带的具体类型
// canfail为true表示目标interface中函数未搜集完整,返回nil,否者直接panic
// 返回值 *itab,携带了目标inter和具体类型的type,和约定和实现约定的函数
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
// 先在itabTable里,一个hash table,查找itab,即inter+typ
m = itabTable.find(inter, typ);
// 未找到的话:
// 申请itab的内存,因为要往func中填函数入口地址,数量是interface中函数个数,所以动态申请
// len(inter.mhdr)-1 此处减一是因为itab中的"fun [1]uintptr"已经占有一个指针大小了。
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() //根据inter中约定的函数,遍历查找typ中的函数入口,并依次添加到itab.func中
itabAdd(m) // 把itab添加到hash table中,宫后续查找
unlock(&itabLock)
finish:
//目标interface中函数全部找到,返回itab
// 未找到,或者未找全,返回或者panic
}
m.init()这个函数就是根据interface中约定的函数,逐个在具体类型中的方法查找,并把找到的函数入口地址,依次放到itab.func指向的数组中去。当任何一个函数未找到的话,让itab.func[0]=0,供外出调用判断是否转换成功。
// init fills in the m.fun array with all the code pointers for
// the m.inter/m._type pair. If the type does not implement the interface,
// it sets m.fun[0] to 0 and returns the name of an interface function that is missing.
// It is ok to call this multiple times on the same m, even concurrently.
func (m *itab) init() string {
inter := m.inter //目标interface
typ := m._type // 具体类型
x := typ.uncommon() // x中有具体类型的所有方法
methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
// 第一层循环目标interface约定的函数
for k := 0; k < ni; k++ {
//...
//第二层循环具体类型的方法
for ; j < nt; j++ {
// 找到匹配的函数,往itab.func指向的数组中填函数入口
methods[k] = ifn
}
// didn't find method
m.fun[0] = 0
return iname
}
m.fun[0] = uintptr(fun0)
return ""
}
- 源interface中的函数是子集
再看源interface中的函数是目标interface的子集的情况。IPeople1中有个GetHeight函数,源interface IPeople中是没有这个函数的。
type IPeople interface {
GetName() string
GetAge() int
}
type IPeople1 interface {
GetName() string
GetAge() int
GetHeight() int
}
func test1(p IPeople1) {
fmt.Printf("Name: %s, Age: %d, Height:%d \n", p.GetName(), p.GetAge(), p.GetHeight())
}
func test(p IPeople) {
test1(p) // cannot use p (type IPeople) as type IPeople1 in argument to test1:IPeople does not implement IPeople1 (missing GetHeight method)
fmt.Printf("Name: %s, Age: %d \n", p.GetName(), p.GetAge())
}
根据上面的getitab和init函数分析,真正查找函数的时候,是根据查找具体类型中的方法集来判断的,那么即使源interface中没有目标中的函数,只要具体类型中有,按理说也会转换成功的。但是,在编译的时候就早早的报错了:
cannot use p (type IPeople) as type IPeople1 in argument to test1:
IPeople does not implement IPeople1 (missing GetHeight method)
所以被编译挡住了,那么可以通过类型断言的方式,先转回具体类型People,再来转换到目标interface IPeople1,这样是可行的。
func test(p IPeople) {
people := p.(People)
test1(people) // cannot use p (type IPeople) as type IPeople1 in argument to test1:IPeople does not implement IPeople1 (missing GetHeight method)
}
3. 空interface类型
相对于非空interface,空interface比较简单,它因为没有约定任何函数,所以任何对象都满足这个约定,都可以转换成空interface。先看看是定义,比较简单:
// empty interface
type eface struct {
_type *_type
data unsafe.Pointer
}
因为是空的约定,所以相比较于非空interface iface中,关于interface的数据结构都没了,保存实现约定的函数集func也不需要了,只保留了具体类型的type和data
4. 是nil吗
下面是The Go Programming Language这本书中的关于interface变量是否是nil的一个例子:
const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // NOTE: subtly incorrect!
if debug {
// ...use buf...
}
}
// If out is non-nil, output will be written to it.
func f(out io.Writer) {
// ...do something...
if out != nil {
out.Write([]byte("done!\n"))
}
}
当debug是false的时候,传给f的是个具体类型为*byte.Buffer, 值为nil的变量,经过runtime.convT2I做了具体类型到interface类型的转换,out不是nil, 只是data为nil而已,只有当out是nil的时候,它才是nil(感觉是废话)。
5. 最后
对于看了一些资料还是比较模糊的知识,最好能手动调试下,或者看一下源代码,这样能真正去理解背后的知识。
我们说接口是一种约定,这个约定背后,源码的设计者们确实做了很多工作,把抽象的约定,提炼并实现出来,源码很巧妙,也很容易理解。