06.Golang 接口(interface)源码分析

Golang 接口(interface)源码分析

注意当前go版本代码为1.23

前言

接口是高级语言中的一个规约,是一组方法签名的集合。在Golang中, 接口是非侵入式的,具体类型实现 interface 不需要在语法上显式的声明,只需要具体类型的方法集合是 interface 方法集合的超集,就表示该类实现了这一 interface。编译器在编译时会进行 interface 校验。interface 和具体类型不同,它不能实现具体逻辑,也不能定义字段。

底层数据结构

Golang中接口的底层结构体有两种,分别是ifaceeface,其中 runtime.iface 描述的接口包含方法,而 runtime.eface 则是不包含任何方法的空接口:interface{}

iface源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// iface 表示一个接口值。它包含指向接口表 (itab) 的指针和指向接口底层具体数据 (data) 的指针。
type iface struct {
tab *abi.ITab // 指向接口表的指针。接口表存储了接口类型信息和实现该接口的具体类型信息。
data unsafe.Pointer // 指向接口底层具体数据的指针。可以指向任何类型的数据。
}

// ITab 代表一个接口表 (Interface Table)。它存储了接口类型和具体类型的信息,以及一个函数指针数组,用于调用具体类型的方法。
type ITab struct {
Inter *InterfaceType // 指向接口类型的指针,是接口的类型定义。描述了接口本身,包括接口的名称、包路径和方法列表。
Type *Type // 指向具体类型的指针,是实现该接口的类型的指针。描述了实现接口的具体类型,例如 *os.File、int 等。
Hash uint32 // 具体类型的哈希值,与 Type.Hash 相同。用于类型 switch 的快速比较。
Fun [1]uintptr // 函数指针数组,存储了具体类型实现的接口方法的地址。这是一个变长数组,实际大小取决于接口方法的数量。fun[0] == 0 表示该类型未实现此接口。
}

// InterfaceType 描述了一个接口类型。
type InterfaceType struct {
Type Type // 继承自 Type,包含类型的基本信息,例如大小、对齐方式等。
PkgPath Name // 接口所属的包的导入路径,例如 "fmt"、"os" 等。
Methods []Imethod // 接口定义的方法列表,按哈希值排序。
}


// Type 描述 Go 中的一个类型。它包含类型的大小、对齐、哈希值等信息。
type Type struct {
Size_ uintptr // 类型的大小,以字节为单位。
PtrBytes uintptr // 类型中可以包含指针的前缀字节数。用于垃圾回收等。
Hash uint32 // 类型的哈希值。用于快速类型比较。
TFlag TFlag // 额外的类型信息标志,例如是否为接口类型、是否为指针类型等。
Align_ uint8 // 变量与此类型的对齐方式,以字节为单位。
FieldAlign_ uint8 // 结构体字段与此类型的对齐方式,以字节为单位。
Kind_ Kind // 类型的种类,例如 int、string、struct 等。这是一个枚举类型。
// 比较此类型对象的函数。
// (指向对象 A 的指针,指向对象 B 的指针) -> 相等吗?
Equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较两个该类型对象是否相等的函数。
// GCData 存储垃圾收集器的 GC 类型数据。
// 如果 kind 中设置了 KindGCProg 位,则 GCData 是一个 GC 程序。
// 否则它是一个 ptrmask 位图。有关详细信息,请参阅 mbitmap.go。
GCData *byte // 指向 GC 数据的指针。用于垃圾回收。
Str NameOff // 类型的字符串表示形式的偏移量。
PtrToThis TypeOff // 指向此类型的指针的类型的偏移量,可能为零。
}

iface 内部维护两个指针,tab 中存放的是类型、方法等信息。data 则指向接口具体的值,一般而言是一个指向堆内存的指针。

由于 Go 语言是强类型语言,编译时对每个变量的类型信息做强校验,所以每个类型的元信息要用一个结构体描述。再者 Go 的反射也是基于类型的元信息实现的。Type 就是所有类型最原始的元信息。

整体结构如下:

iface 结构体全景

eface源码

源码路径: runtime.eface

1
2
3
4
5
6
type eface struct {
_type *_type
data unsafe.Pointer
}

type _type = abi.Type

eface作为空的 inferface{} 是没有方法集的接口。所以不需要 itab 数据结构。它只需要存类型和类型对应的值即可。对应的数据结构如下:

从这个数据结构可以看出,相比 ifaceeface 只需要维护 abi.Type ,表示空接口所承载的具体的实体类型和data 描述了具体的值。它们分别被称为动态类型动态值。而接口值包括动态类型动态值,只有当 2 个字段都为 nil,空接口才为 nil。空接口的主要目的有 2 个,一是实现“泛型”,二是使用反射。所以空接口并不一定等于nil,这是常见的犯错点。

关于type的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Type 描述 Go 中的一个类型。它包含类型的大小、对齐、哈希值等信息。
type Type struct {
Size_ uintptr // 类型的大小,以字节为单位。
PtrBytes uintptr // 类型中可以包含指针的前缀字节数。用于垃圾回收等。
Hash uint32 // 类型的哈希值。用于快速类型比较。
TFlag TFlag // 额外的类型信息标志,例如是否为接口类型、是否为指针类型等。
Align_ uint8 // 变量与此类型的对齐方式,以字节为单位。
FieldAlign_ uint8 // 结构体字段与此类型的对齐方式,以字节为单位。
Kind_ Kind // 类型的种类,例如 int、string、struct 等。这是一个枚举类型。
// 比较此类型对象的函数。
// (指向对象 A 的指针,指向对象 B 的指针) -> 相等吗?
Equal func(unsafe.Pointer, unsafe.Pointer) bool // 比较两个该类型对象是否相等的函数。
// GCData 存储垃圾收集器的 GC 类型数据。
// 如果 kind 中设置了 KindGCProg 位,则 GCData 是一个 GC 程序。
// 否则它是一个 ptrmask 位图。有关详细信息,请参阅 mbitmap.go。
GCData *byte // 指向 GC 数据的指针。用于垃圾回收。
Str NameOff // 类型的字符串表示形式的偏移量。
PtrToThis TypeOff // 指向此类型的指针的类型的偏移量,可能为零。
}

Go 语言各种数据类型都是在 _type 字段的基础上,增加一些额外的字段来进行管理的,这些数据类型的结构体定义,也是反射实现的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//数组类型
type ArrayType struct {
Type
Elem *Type // array element type
Slice *Type // slice type
Len uintptr
}

//通道类型
type ChanType struct {
Type
Elem *Type
Dir ChanDir
}

//切片类型
type SliceType struct {
Type
Elem *Type // slice element type
}

//结构体类型
type StructType struct {
Type
PkgPath Name
Fields []StructField
}

问题

1.空接口一定等于 nil 吗?

接口值的零值是指动态类型动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Coder interface {
}

type Gopher struct {
name string
}

func main() {
var c Coder
//输出: true
fmt.Println(c == nil)
//输出: c: <nil>, <nil>
fmt.Printf("c: %T, %v\n", c, c)

var g *Gopher
//输出: true
fmt.Println(g == nil)

c = g
//输出: false
fmt.Println(c == nil)
//输出: c: *main.Gopher, <nil>
fmt.Printf("c: %T, %v\n", c, c)
}

2.【引申】 fmt.Println 函数的参数是 interface。对于内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。而对于自定义类型,首先确定该类型是否实现了 String() 方法,如果实现了,则直接打印输出 String() 方法的结果;否则,会通过反射来遍历对象的成员进行打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Student struct {
Name string
Age int
}

func main() {
var s = Student{
Name: "test",
Age: 18,
}
// 输出: {test 18}
fmt.Println(s)
}

因为 Student 结构体没有实现 String() 方法,所以 fmt.Println 会利用反射挨个打印成员变量:{qcrao 18}

这是添加string()的实现:

1
2
3
func (s Student) String() string {
return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}

打印结果:

1
[Name: test], [Age: 18]

但是如果是*Student为接受者类型呢?

1
2
3
func (s *Student) String() string {
return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}

打印结果:

1
{test 18}

原因:类型 T 只有接受者是 T 的方法;而类型 *T 拥有接受者是 T*T 的方法。语法上 T 能直接调 *T 的方法仅仅是 Go 的语法糖。

所以Student 结构体定义了接受者类型是值类型的 String() 方法时,通过**fmt.Println(s)或者fmt.Println(&s)**均可以按照自定义的格式来打印。

如果 Student 结构体定义了接受者类型是指针类型的 String() 方法时,只有通过**fmt.Println(&s)**才能按照自定义的格式打印。

参考链接

1.iface与eface的区别是什么

2.深入研究 Go interface 底层实现


06.Golang 接口(interface)源码分析
https://blog.longpi1.com/2024/12/01/06-Golang-接口(interface)源码分析/