加入收藏
举报
02-15 09:22
#0
文件名称:
06.Golang 接口(interface)源码分析.md
所在目录:
编程语言 / Golang / 源码分析
文件大小:
9.87 KB
下载地址:
longpi1/Reading-notes
   
免责声明:本网站仅提供指向 GitHub 上的文件的链接,所有文件的版权归原作者所有,本网站不对文件内容的合法性、准确性或安全性承担任何责任。
文本预览:
# Golang 接口(interface)源码分析
> 注意当前go版本代码为1.23
## 前言
接口是高级语言中的一个规约,是一组方法签名的集合。在Golang中, 接口是非侵入式的,具体类型实现 interface 不需要在语法上显式的声明,只需要具体类型的方法集合是 interface 方法集合的超集,就表示该类实现了这一 interface。编译器在编译时会进行 interface 校验。interface 和具体类型不同,它不能实现具体逻辑,也不能定义字段。
## 底层数据结构
Golang中接口的底层结构体有两种,分别是`iface` 和 `eface`,其中 [`runtime.iface`](https://draveness.me/golang/tree/runtime.iface) 描述的接口包含方法,而 [`runtime.eface`](https://draveness.me/golang/tree/runtime.eface) 则是不包含任何方法的空接口:`interface{}`。
### iface源码
```golang
// 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 结构体全景](https://golang.design/go-questions/interface/assets/0.png)
### eface源码
源码路径: [`runtime.eface`](https://draveness.me/golang/tree/runtime.eface)
```golang
type eface struct {
_type *_type
data unsafe.Pointer
}
type _type = abi.Type
```
eface作为空的 inferface{} 是没有方法集的接口。所以不需要 itab 数据结构。它只需要存类型和类型对应的值即可。对应的数据结构如下:
从这个数据结构可以看出,相比 `iface`,`eface` 只需要维护 `abi.Type` ,表示空接口所承载的具体的实体类型和`data` 描述了具体的值。它们分别被称为`动态类型`和`动态值`。而接口值包括`动态类型`和`动态值`,只有当 2 个字段都为 nil,空接口才为 nil。空接口的主要目的有 2 个,一是实现“泛型”,二是使用反射。**所以空接口并不一定等于nil**,这是常见的犯错点。
### 关于type的结构体
```golang
// 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` 字段的基础上,增加一些额外的字段来进行管理的,这些数据类型的结构体定义,也是反射实现的基础。
```golang
//数组类型
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`。
```go
type Coder interface {
}
type Gopher struct {
name string
}
func main() {
var c Coder
//输出: true
fmt.Println(c == nil)
//输出: c: ,
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,
fmt.Printf("c: %T, %v\n", c, c)
}
```
2.【引申】 `fmt.Println` 函数的参数是 `interface`。对于内置类型,函数内部会用穷举法,得出它的真实类型,然后转换为字符串打印。而对于自定义类型,首先确定该类型是否实现了 `String()` 方法,如果实现了,则直接打印输出 `String()` 方法的结果;否则,会通过反射来遍历对象的成员进行打印。
```go
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()的实现:
```golang
func (s Student) String() string {
return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}
```
打印结果:
```shell
[Name: test], [Age: 18]
```
但是如果是*Student为接受者类型呢?
```go
func (s *Student) String() string {
return fmt.Sprintf("[Name: %s], [Age: %d]", s.Name, s.Age)
}
```
打印结果:
```shell
{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的区别是什么](https://golang.design/go-questions/interface/iface-eface/)
2.[深入研究 Go interface 底层实现](https://halfrost.com/go_interface/)
点赞 回复
回帖
支持markdown部分语法 ?