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定义的函数集,即约定
}

一图胜千言:

iface相关数据结构

通过上面的代码和图片可以看到,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. 最后

对于看了一些资料还是比较模糊的知识,最好能手动调试下,或者看一下源代码,这样能真正去理解背后的知识。

我们说接口是一种约定,这个约定背后,源码的设计者们确实做了很多工作,把抽象的约定,提炼并实现出来,源码很巧妙,也很容易理解。