我们先把版本切换到1.14.7
之前的源码分析都是基于此版本的,我们切换过来~
接口是高级语言中的一个规约,是一组方法签名的集合。golang中,接口本身也是一种类型,它代表的是一个方法的集合。任何类型只要实现了接口中声明的所有方法,那么该类就实现了该接口。与其他语言不同,golang并不需要显示声明类型实现了某个接口,而是由编译器和runtime进行检查。不用显示什么这点非常棒,这样就无侵入非常方便。
神奇的nil != nil~
package main
import "fmt"
type NilError struct{}
func (*NilError) Error() string {
return "my error"
}
func TestError(n int) error {
var e *NilError
if n < 0{
e = new(NilError)
}
if e == nil {
fmt.Println("e is nil")
}
return e
}
func main() {
var err,err1 error
if err == nil{
fmt.Println("现在是nil")
err1 =err
}
err = TestError(5)
fmt.Println(err == nil) //false
fmt.Println(err == err1) //false
}
运行结果:
现在是nil
e is nil
false
falseTestErr返回的明明是个nil呀,为什么返回值 err == nil不好使了呢?这就是奇怪的nil != nil,这个问题很多刚转go的人可能会被坑到~
带着这个问题,我们开始接下来的内容。
接口的值
接口的值简单来说,是由两部分组成的,就是类型和数据,详细的组成会在下面的实现章节中说明。
那么判断两个接口是相等,就是看他们的这两部分是否相等;另外类型和数据都为nil才代表接口是nil,这里就解释了上面的问题。由于golang的err实现是一个接口,所以很容易在err的处理过程中写错,这里要特别注意,
实现
在golang中你不再需要和java这样的语言一样声明一个类型实现了哪些接口,这带来了开发上的方便,但是实现上会比那些需要声明的语言更加复杂。总的来说golang的接口检测既有静态部分,也有动态部分。
- 静态部分
对于具体类型(concrete type,包括自定义类型) -> interface,编译器生成对应的itab放到ELF的.rodata段,后续要获取itab时,直接把指针指向存在.rodata的相关偏移地址即可。
对于interface->具体类型(concrete type,包括自定义类型),编译器提取相关字段进行比较,并生成值 - 动态部分
在runtime中会有一个全局的hash表,记录了相应type->interface类型转换的itab,进行转换时候,先到hash表中查,如果有就返回成功;如果没有,就检查这两种类型能否转换,能就插入到hash表中返回成功,不能就返回失败。注意这里的hash表不是go中的map,而是一个最原始的使用数组的hash表,使用开放地址法来解决冲突。主要是interface <-> interface(接口赋值给接口、接口转换成另一接口)使用到动态生产itab。
后面我们会详细分析itabTable。
数据结构
对于golang来说有两种接口的结构,一种是有方法定义的接口,一种是空接口,分别对应两种实现。
我们分别定义一个空接口和非空接口 我们用gdb debug看下 到底是神马东西~
package main
import (
"fmt"
"io"
"os"
)
func main(){
var e interface{}
var rw io.ReadWriter
f, _ := os.Open("hello.txt")
e = f
rw = f
fmt.Print(e)
fmt.Print(rw)
}
e是一个空interface
rw是下面这样一个非空的interface
type ReadWriter interface {
Reader
Writer
}我们gdb调试看一下
go build -gcflags '-N -l' -o hello hello.go
(gdb) p e
$7 = {_type = 0x0, data = 0x0}
(gdb) ptype e
type = struct runtime.eface {
runtime._type *_type;
void *data;
}efacesrc/runtime/runtime2.gotype eface struct {
_type *_type //类型元数据
data unsafe.Pointer //数据信息,指向数据指针
}
eface包含了2个元素,一个是_type,指向对象的类型元数据,一个 data,数据指针
_typesrc/runtime/type.go Go 语言是强类型语言,编译时对每个变量的类型信息做强校验,所以每个类型的元信息要用一个结构体描述。再者 Go 的反射也是基于类型的元信息实现的。_type 就是所有类型最原始的元信息。
像类型名称,大小,对齐边界,是否为自定义类型等信息,是每个类型元数据都要记录的。所以被放到了runtime._type结构体中,作为每个类型元数据的Header。
type _type struct {
size uintptr // 类型占用内存大小
ptrdata uintptr // 包含所有指针的内存前缀大小
hash uint32 // 类型 hash
tflag tflag // 标记位,主要用于反射
align uint8 // 对齐字节信息
fieldAlign uint8 // 当前结构字段的对齐字节数
kind uint8 // 基础类型枚举值
equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较两个形参对应对象的类型是否相等
gcdata *byte // GC 类型的数据
str nameOff // 类型名称字符串在二进制文件段中的偏移量
ptrToThis typeOff // 类型元信息指针在二进制文件段中的偏移量
}
三个字段我们重点解释一下,其余的都在备注里进行了说明:
- kind,这个字段描述的是如何解析基础类型。在 Go 语言中,基础类型是一个枚举常量,有 26 个基础类型,如下。枚举值通过 kindMask 取出特殊标记位。
const (
kindBool = 1 + iota
kindInt
kindInt8
kindInt16
kindInt32
kindInt64
kindUint
kindUint8
kindUint16
kindUint32
kindUint64
kindUintptr
kindFloat32
kindFloat64
kindComplex64
kindComplex128
kindArray
kindChan
kindFunc
kindInterface
kindMap
kindPtr
kindSlice
kindString
kindStruct
kindUnsafePointer
kindDirectIface = 1 << 5
kindGCProg = 1 << 6
kindMask = (1 << 5) - 1
)
2. str 和 ptrToThis,对应的类型是 nameoff 和 typeOff。分表表示name和type针对最终输出文件所在段内的偏移量。在编译的链接步骤中,链接器将各个 .o 文件中的段合并到输出文件,会进行段合并,有的放入 .text 段,有的放入 .data 段,有的放入 .bss 段。nameoff和typeoff就是记录了对应段的偏移量。
_type类型是所有类型的类型元数据的header,我们来看golang内置的几个类型元数据加深下理解。
type ptrtype struct {
typ _type
elem *_type
}
指针类型的元数据,它在_type后面也额外存储了一个*_type,指向指针类型指向的那个类型的元数据。
type slicetype struct {
typ _type
elem *_type
}
slice的类型元数据在_type结构体后面,记录着一个*_type,指向其存储的元素的类型元数据。如果是存储string的slice类型,这个指针就指向string类型的元数据。
2. 非空interface
空interface由于不包含方法,所以结构也相对简单,有了空interface的基础,我们来看非空的interface。继续用gdb调试
(gdb) p rw
$8 = {tab = 0x0, data = 0x0}
(gdb) ptype rw
type = struct runtime.iface {
runtime.itab *tab;
void *data;
}
(gdb) ptype rw.tab
type = struct runtime.itab {
runtime.interfacetype *inter;
runtime._type *_type;
uint32 hash;
[4]uint8 _;
[1]uintptr fun;
} *可以看出使用的是runtime.iface这个结构体,我们ctrl+shift+f 在源码的runtime包查找一下。
在runtime/runtime2.go下定义了iface的结构。
// runtime/runtime2.go
// 非空接口
type iface struct {
tab *itab
data unsafe.Pointer //指向原始数据指针
}
tab 中存放的是类型、方法等信息。data 指针指向的 iface 绑定对象的原始数据。
type itab struct {
//inter 和 _type 确定唯一的 _type类型
inter *interfacetype //接口自身定义的类型信息,用于定位到具体interface类型
_type *_type // 接口实际指向值的类型信息-实际对象类型,用于定义具体interface类型
hash uint32 //_type.hash的拷贝,用于快速查询和判断目标类型和接口中类型是一致
_ [4]byte
fun [1]uintptr //动态数组,接口方法实现列表(方法集),即函数地址列表,按字典序排序
//如果数组中的内容为空表示 _type 没有实现 inter 接口
}
- itab.inter是interface的类型元数据,它里面记录了这个接口类型的描述信息,接口要求的方法列表就记录在interfacetype.mhdr这里。
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
tab._type就是接口的动态类型,也就是被赋给接口类型的那个变量的类型元数据。itab 中的 _type 和 iface 中的 data 能简要描述一个变量。_type 是这个变量对应的类型,data 是这个变量的值。
itab.hash是从itab._type中拷贝来的,是类型的哈希值,用于快速判断类型是否相等时使用。
itab.fun记录的是动态类型实现的那些接口要求的方法的地址,是从方法元数据中拷贝来的,为的是快速定位到方法。如果itab._type对应的类型没有实现这个接口,则itab.fun[0]=0,这在类型断言时会用到。当fun[0]为0时,说明_type并没有实现该接口,当有实现接口时,fun存放了第一个接口方法的地址,其他方法一次往下存放,这里就简单用空间换时间,其实方法都在_type字段中能找到,实际在这记录下,每次调用的时候就不用动态查找了。
赋值前后
刚开始我们声明了两个变量 一个是空接口 e 非空接口 io.ReadWriter 类型的rw。
我们看下赋值前的结果
(gdb) p e
$1 = {_type = 0x0, data = 0x0}
(gdb) ptype e
type = struct runtime.eface {
runtime._type *_type;
void *data;
}
(gdb) p rw
$3 = {tab = 0x0, data = 0x0}
(gdb) ptype rw
type = struct runtime.iface {
runtime.itab *tab;
void *data;
}可以看出来不管是空接口的_type还是非空接口的itab这个时候都是nil
这里我们仅用gdb调试到的结果为准
(gdb) n
13 e = f
(gdb) n
14 rw = f
(gdb) p e
$16 = {_type = 0x10c7380 <type.*+170080>, data = 0xc00000e010}我们开始看下e的值 我们把f赋值给了e
如图所示,因为f本身就是个指针,所以e这里的data就等于f,动态类型就是*os.File。
在看rw的值 rw是一个非空指针
(gdb) p rw
$23 = {tab = 0x10eaee0 <File,io.ReadWriter>, data = 0xc00000e010}rw的动态值就是f,动态类型就是*os.File。而itab.fun这个数组里记录的是*os.File实现的Read、Write方法的地址。
本篇简单的介绍了一下接口常见的使用场景,简单介绍了一下接口的实现,以及接口的数据机构,用gdb调试的方式向大家展示了一下接口赋值前和赋值后的区别;
接下来我们要开始分析全局itabinit itabTable、接口的类型转换、类型断言 Type Assertion、类型查询 Type Switches!