接口运行时的表示
# 3. 接口运行时的表示
**接口是 Go 这门静态语言中唯一“动静兼备”的语法特性,**而且,接口“动静兼备”的特性给 Go 带来了强大的表达能力。
# 3.1 接口的静态特性与动态特性
接口的静态特性体现在接口类型变量具有静态类型,比如var err error中变量 err 的静态类型为 error。
拥有静态类型,那就意味着编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法。如果不满足,就会报错:
var err error = 1 // cannot use 1 (type int) as type error in assignment: int does not implement error (missing Error method)
接口的动态特性体现在接口类型变量在运行时还存储了右值的真实类型信息,这个右值的真实类型被称为接口类型变量的动态类型。示例代码:
var err error
err = errors.New("error1")
fmt.Printf("%T\n", err) // *errors.errorString
2
3
通过 errros.New 构造了一个错误值,赋值给了 error 接口类型变量 err,并通过 fmt.Printf 函数输出接口类型变量 err 的动态类型为 *errors.errorString。
“动静皆备”的特性好处:
**好处1:**接口类型变量在程序运行时可以被赋值为不同的动态类型变量,每次赋值后,接口类型变量中存储的动态类型信息都会发生变化,这让 Go 语言可以像动态语言(比如 Python)那样拥有使用Duck Typing(鸭子类型)的灵活性。
鸭子类型 (opens new window),就是指某类型所表现出的特性(比如是否可以作为某接口类型的右值),不是由其基因(比如 C++ 中的父类)决定的,而是由类型所表现出来的行为(比如类型拥有的方法)决定的。
type QuackableAnimal interface {
Quack()
}
type Duck struct{}
func (Duck) Quack() {
println("duck quack!")
}
type Dog struct{}
func (Dog) Quack() {
println("dog quack!")
}
type Bird struct{}
func (Bird) Quack() {
println("bird quack!")
}
func AnimalQuackInForest(a QuackableAnimal) {
a.Quack()
}
func main() {
animals := []QuackableAnimal{new(Duck), new(Dog), new(Bird)}
for _, animal := range animals {
AnimalQuackInForest(animal)
}
}
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
用接口类型 QuackableAnimal 来代表具有“会叫”这一特征的动物,而 Duck、Bird 和 Dog 类型各自都具有这样的特征,于是可以将这三个类型的变量赋值给 QuackableAnimal 接口类型变量 a。
每次赋值,变量 a 中存储的动态类型信息都不同,Quack 方法的执行结果将根据变量 a 中存储的动态类型信息而定。这里的 Duck、Bird、Dog 都是“鸭子类型”,但它们之间并没有什么联系,之所以能作为右值赋值给 QuackableAnimal 类型变量,只是因为他们表现出了 QuackableAnimal 所要求的特征罢了。
好处2:Go 接口:可以保证“动态特性”使用时的安全性。
比如,编译器在编译期就可以捕捉到将 int 类型变量传给 QuackableAnimal 接口类型变量这样的明显错误,决不会让这样的错误遗漏到运行时才被发现。
# 3.2 经典困惑:“nil error 值 != nil”
type MyError struct {
error
}
var ErrBad = MyError{
error: errors.New("bad things happened"),
}
func bad() bool {
return false
}
func returnsError() error {
var p *MyError = nil
if bad() {
p = &ErrBad
}
return p
}
func main() {
err := returnsError()
if err != nil {
fmt.Printf("error occur: %+v\n", err)
return
}
fmt.Println("ok")
}
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
returnsError 这个函数定义了一个*MyError类型的变量 p,初值为 nil。如果函数 bad 返回 false,returnsError 函数就会直接将 p(此时 p = nil)作为返回值返回给调用者,之后调用者会将 returnsError 函数的返回值(error 接口类型)与 nil 进行比较,并根据比较结果做出最终处理。一般处理思路大概是这样的:p 为 nil,returnsError 返回 p,那么 main 函数中的 err 就等于 nil,于是程序输出 ok 后退出。
真实的运行结果:
error occur: <nil>
1示例程序并未如预期的那样输出 ok。程序显然是进入了错误处理分支,输出了 err 的值。
returnsError 返回的 error 接口类型变量 err 的数据指针虽然为空,但它的类型信息(iface.tab)并不为空,而是 *MyError 对应的类型信息,这样 err 与 nil(0x0,0x0)相比自然不相等
# 3.2.1 接口类型变量的内部表示
接口类型“动静兼备”的特性也决定了它的变量的内部表示绝不像一个静态类型变量(如 int、float64)那样简单。
可以在$GOROOT/src/runtime/runtime2.go中找到接口类型变量在运行时的表示:
// $GOROOT/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
// $GOROOT/src/runtime/runtime2.go
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
// $GOROOT/src/runtime/type.go
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
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
在运行时层面,接口类型变量有两种内部表示:iface和eface,这两种表示分别用于不同的接口类型变量:
- eface 用于表示没有方法的空接口(empty interface)类型变量,也就是 interface{}类型的变量;
- iface 用于表示其余拥有方法的接口 interface 类型变量。
共同点:
都有两个指针字段,并且第二个指针字段的功能相同,都是指向当前赋值给该接口类型变量的动态类型变量的值。
不同点:
- eface 表示的空接口类型并没有方法列表,因此它的第一个指针字段指向一个_type类型结构,这个结构为该接口类型变量的动态类型的信息。
- iface 除了要存储动态类型信息之外,还要存储接口本身的信息(接口的类型信息、方法列表信息等)以及动态类型所实现的方法的信息,因此 iface 的第一个字段指向一个itab类型结构。
直观展现 eface 和 iface 的结构
eface 表示的空接口类型变量:
type T struct {
n int
s string
}
func main() {
var t = T {
n: 17,
s: "hello, interface",
}
var ei interface{} = t // Go运行时使用eface结构表示ei
}
2
3
4
5
6
7
8
9
10
11
12
13
空接口类型变量 ei 在 Go 运行时的表示是这样的:
上半部分 _type 字段指向它的动态类型 T 的类型信息,下半部分的 data 则是指向一个 T 类型的实例值。
iface 表示非空接口类型变量:
type T struct {
n int
s string
}
func (T) M1() {}
func (T) M2() {}
type NonEmptyInterface interface {
M1()
M2()
}
func main() {
var t = T{
n: 18,
s: "hello, interface",
}
var i NonEmptyInterface = t
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NonEmptyInterface 接口类型变量在 Go 运行时表示的示意图:
每个接口类型变量在运行时的表示都是由两部分组成的,针对不同接口类型可以简化记作:eface(_type, data)和iface(tab, data)。
接口类型变量的 tab 和 _type 可以统一看作是动态类型的类型信息。Go 语言中每种类型都会有唯一的 _type 信息,无论是内置原生类型,还是自定义类型都有。Go 运行时会为程序内的全部类型建立只读的共享 _type 信息表,因此拥有相同动态类型的同类接口类型变量的 _type/tab 信息是相同的。
接口类型变量的 data 部分则是指向一个动态分配的内存空间,这个内存空间存储的是赋值给接口类型变量的动态类型变量的值。未显式初始化的接口类型变量的值为nil,也就是这个变量的 _type/tab 和 data 都为 nil。
判断两个接口类型变量是否相同,只需要判断 _type/tab 是否相同,以及 data 指针指向的内存空间所存储的数据值是否相同。
第一种:nil 接口变量
未赋初值的接口类型变量的值为 nil,这类变量也就是 nil 接口变量,这类变量内部表示输出:
func printNilInterface() {
// nil接口变量
var i interface{} // 空接口类型
var err error // 非空接口类型
println(i)
println(err)
println("i = nil:", i == nil)
println("err = nil:", err == nil)
println("i = err:", i == err)
}
/*
(0x0,0x0)
(0x0,0x0)
i = nil: true
err = nil: true
i = err: true
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
无论是空接口类型还是非空接口类型变量,一旦变量值为 nil,那么它们内部表示均为(0x0,0x0),也就是类型信息、数据值信息均为空。因此上面的变量 i 和 err 等值判断为 true。
第二种:空接口类型变量
空接口类型变量的内部表示输出:
func printEmptyInterface() {
var eif1 interface{} // 空接口类型
var eif2 interface{} // 空接口类型
var n, m int = 17, 18
eif1 = n
eif2 = m
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2) // false
eif2 = 17
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2) // true
eif2 = int64(17)
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2) // false
}
/*
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0xc00007ef40)
eif1 = eif2: false
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0x10eb3d0)
eif1 = eif2: true
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac640,0x10eb3d8)
eif1 = eif2: false
*/
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
首先,代码执行到第 11 行时,eif1 与 eif2 已经分别被赋值整型值 17 与 18,这样 eif1 和 eif2 的动态类型的类型信息是相同的(都是 0x10ac580),但 data 指针指向的内存块中存储的值不同,一个是 17,一个是 18,于是 eif1 不等于 eif2。
接着,代码执行到第 16 行的时候,eif2 已经被重新赋值为 17,这样 eif1 和 eif2 不仅存储的动态类型的类型信息是相同的(都是 0x10ac580),data 指针指向的内存块中存储值也相同了,都是 17,于是 eif1 等于 eif2。
然后,代码执行到第 21 行时,eif2 已经被重新赋值了 int64 类型的数值 17。这样,eif1 和 eif2 存储的动态类型的类型信息就变成不同的了,一个是 int,一个是 int64,即便 data 指针指向的内存块中存储值是相同的,最终 eif1 与 eif2 也是不相等的。
从输出结果中总结:**对于空接口类型变量,只有 _type 和 data 所指数据内容一致的情况下,两个空接口类型变量之间才能划等号。**另外,Go 在创建 eface 时一般会为 data 重新分配新内存空间,将动态类型变量的值复制到这块内存空间,并将 data 指针指向这块内存空间。因此多数情况下看到的 data 指针值都是不同的。
第三种:非空接口类型变量
非空接口类型变量的内部表示输出:
type T int
func (t T) Error() string {
return "bad error"
}
func printNonEmptyInterface() {
var err1 error // 非空接口类型
var err2 error // 非空接口类型
err1 = (*T)(nil)
println("err1:", err1)
println("err1 = nil:", err1 == nil)
err1 = T(5)
err2 = T(6)
println("err1:", err1)
println("err2:", err2)
println("err1 = err2:", err1 == err2)
err2 = fmt.Errorf("%d\n", 5)
println("err1:", err1)
println("err2:", err2)
println("err1 = err2:", err1 == err2)
}
/*
err1: (0x10ed120,0x0)
err1 = nil: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed1a0,0x10eb318)
err1 = err2: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed0c0,0xc000010050)
err1 = err2: false
*/
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
每一轮通过 println 输出的 err1 和 err2 的 tab 和 data 值,要么 data 值不同,要么 tab 与 data 值都不同。
和空接口类型变量一样,只有 tab 和 data 指的数据内容一致的情况下,两个非空接口类型变量之间才能划等号。
err1 下面的赋值情况:
err1 = (*T)(nil)
1针对这种赋值,println 输出的 err1 是(0x10ed120, 0x0),也就是非空接口类型变量的类型信息并不为空,数据指针为空,因此它与 nil(0x0,0x0)之间不能划等号。
第四种:空接口类型变量与非空接口类型变量的等值比较
func printEmptyInterfaceAndNonEmptyInterface() {
var eif interface{} = T(5)
var err error = T(5)
println("eif:", eif)
println("err:", err)
println("eif = err:", eif == err)
err = T(6)
println("eif:", eif)
println("err:", err)
println("eif = err:", eif == err)
}
/*
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4d8)
eif = err: true
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4e0)
eif = err: false
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
空接口类型变量和非空接口类型变量内部表示的结构有所不同(第一个字段:_type vs . tab),两者似乎一定不能相等。但 Go 在进行等值比较时,类型比较使用的是 eface 的 _type 和 iface 的 tab._type,当 eif 和 err 都被赋值为T(5)时,两者之间是划等号的。
输出接口类型变量内部表示的详细信息
println 输出的接口类型变量的内部表示信息,在一般情况下都是足够的,但有些时候又显得过于简略,比如在上面最后一个例子中,如果仅凭eif: (0x10b3b00,0x10eb4d0)和err: (0x10ed380,0x10eb4d8)的输出,是无法想到两个变量是相等的。
需要输出接口类型变量内部表示的详细信息(比如:tab._type)
# 3.3 接口类型的装箱(boxing)原理
装箱(boxing) 是编程语言领域的一个基础概念,一般是指把一个值类型转换成引用类型,比如在支持装箱概念的 Java 语言中,将一个 int 变量转换成 Integer 对象就是一个装箱操作。
在 Go 语言中,将任意类型赋值给一个接口类型变量也是 装箱 操作。接口类型的装箱实际就是创建一个 eface 或 iface 的过程。
// interface_internal.go
type T struct {
n int
s string
}
func (T) M1() {}
func (T) M2() {}
type NonEmptyInterface interface {
M1()
M2()
}
func main() {
var t = T{
n: 17,
s: "hello, interface",
}
var ei interface{}
ei = t
var i NonEmptyInterface
i = t
fmt.Println(ei)
fmt.Println(i)
}
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
对 ei 和 i 两个接口类型变量的赋值都会触发装箱操作。
装箱是一个有性能损耗的操作,因此 Go 也在不断对装箱操作进行优化,包括对常见类型如整型、字符串、切片等提供系列快速转换函数:
// $GOROOT/src/runtime/iface.go
func convT16(val any) unsafe.Pointer // val must be uint16-like
func convT32(val any) unsafe.Pointer // val must be uint32-like
func convT64(val any) unsafe.Pointer // val must be uint64-like
func convTstring(val any) unsafe.Pointer // val must be a string
func convTslice(val any) unsafe.Pointer // val must be a slice
2
3
4
5
6
这些函数去除了 typedmemmove 操作,增加了零值快速返回等特性。
同时 Go 建立了 staticuint64s 区域,对 255 以内的小整数值进行装箱操作时不再分配新内存 (opens new window),而是利用 staticuint64s 区域的内存空间,下面是 staticuint64s 的定义:
// $GOROOT/src/runtime/iface.go
// staticuint64s is used to avoid allocating in convTx for small integer values.
var staticuint64s = [...]uint64{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
... ...
}
2
3
4
5
6
7