Go-interface底层原理剖析

270 阅读3分钟

golang中,通过接口(interface)来实现代码多态的功能,解耦上下文,提高代码的可读性,在平常的架构中经常使用,下面我们来看一下interface各种操作的具体实现流程

老规矩先看结构体

结构体

对于空接口(interface{})和非空接口的结构体是不一样的

// 带方法的接口
type iface struct {
   tab  *itab          //接口类型和具体类型的组合
   data unsafe.Pointer //存储数据的指针
}

// 不带方法的接口(空接口)
type eface struct {
   _type *_type
   data  unsafe.Pointer
}

type itab struct {
	inter *interfacetype //接口类型元数据
	_type *_type         //实现接口的类型元数据
	hash  uint32         //用于断言匹配类型是否一致的哈希值
	_     [4]byte
	fun   [1]uintptr //实现接口的类型函数地址
}

// 类型的元数据 (详情字段作用可看下方源码注解)
type Type struct {
	Size_       uintptr
	PtrBytes    uintptr 
	Hash        uint32 //存储类型的哈希值,用于快速比较类型是否相等
	TFlag       TFlag   
	Align_      uint8   
	FieldAlign_ uint8  
	Kind_       uint8  
	Equal func(unsafe.Pointer, unsafe.Pointer) bool
	GCData    *byte
	Str       NameOff // string form
	PtrToThis TypeOff // type for pointer to this type, may be zero
}

结构体源码:
itab 结构体定义了非空接口的接口类型数据实现接口的类型信息,并且拷贝的_type结构类型里的hash值用于类型快速判断,以及实现接口的类型函数地址。

_type 结构体定义了数据结构类型的元数据

类型断言

非空接口和空接口类型断言流程差不多,但底层结构体不一样,所以类型断言有一点差别

非空接口

package main

type Duck interface {
   Quack()
}

type Cat struct {
   Name string
}

//go:noinline
func (c *Cat) Quack() {
   println(c.Name + " meow")
}

func main() {
   var c Duck = &Cat{Name: "draven"}
   //类型断言
   switch c.(type) {
   case *Cat:
      cat := c.(*Cat)
      cat.Quack()
   }
}

上面的代码实现流程:

  1. 初始化c接口的值(go语言编译器会去优化初始化流程,如果关闭优化流程,流程会变成,先初始化Cat,然后再将Cat结构体转化为Duck接口的流程)

  2. 然后根据c.tab.hash于目标哈希值进行比较,如果匹配成功就使用c类型中存储的Cat指针,如果匹配不成功就直接恢复栈指针并返回到调用方(可以理解为进行下一个case比对)

    在进行类型断言获取类型值的时候,如果没有进行如v,ok:= c.(*Dog)的判断断言,如果类型跟实际类型不一样,会直接抛出interface conversion: Type1 is Type2, not Type3的panic

空接口

空接口跟非空接口的流程差不多一致,但如果不进行编译优化的话,类型从结构体转化为接口的流程中,接口底层的数据结构是不一样,空接口的实现结构是runtime.eface

总结

interface在我们平常的代码中,经常使用,它抽象出结构体的方法,实现了一个多态的功能,减少了代码量和提高代码的可读性,如工厂模式就是利用了这个抽象的形式,