golang中,通过接口(interface)来实现代码多态的功能,解耦上下文,提高代码的可读性,在平常的架构中经常使用,下面我们来看一下interface各种操作的具体实现流程
老规矩先看结构体
结构体
对于空接口(interface{})和非空接口的结构体是不一样的
- 使用 runtime.iface 结构体表示非空结构体
- 使用 runtime.eface 结构体表示空接口类型;
// 带方法的接口
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()
}
}
上面的代码实现流程:
-
初始化
c接口的值(go语言编译器会去优化初始化流程,如果关闭优化流程,流程会变成,先初始化Cat,然后再将Cat结构体转化为Duck接口的流程) -
然后根据
c.tab.hash于目标哈希值进行比较,如果匹配成功就使用c类型中存储的Cat指针,如果匹配不成功就直接恢复栈指针并返回到调用方(可以理解为进行下一个case比对)在进行类型断言获取类型值的时候,如果没有进行如
v,ok:= c.(*Dog)的判断断言,如果类型跟实际类型不一样,会直接抛出interface conversion: Type1 is Type2, not Type3的panic
空接口
空接口跟非空接口的流程差不多一致,但如果不进行编译优化的话,类型从结构体转化为接口的流程中,接口底层的数据结构是不一样,空接口的实现结构是runtime.eface
总结
interface在我们平常的代码中,经常使用,它抽象出结构体的方法,实现了一个多态的功能,减少了代码量和提高代码的可读性,如工厂模式就是利用了这个抽象的形式,