我们先把版本切换到1.14.7
之前的源码分析都是基于此版本的,我们切换过来~
ps:电脑内存受限 有些版本的golang测试完就删除了~只剩余3个

接口是高级语言中的一个规约,是一组方法签名的集合。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
false

TestErr返回的明明是个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.go
type 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  // 类型元信息指针在二进制文件段中的偏移量
}

三个字段我们重点解释一下,其余的都在备注里进行了说明:

  1. 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!