Go语言精进之路读书笔记第26条——了解接口类型变量的内部表示
接口是Go这门静态语言中唯一“动静兼备”的语言特性
- 接口的静态特性
- 接口类型变量具有静态类型,比如:
var e error
中变量e的静态类型为error - 支持在编译阶段的类型检查:当一个接口类型变量被赋值时,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法
- 接口类型变量具有静态类型,比如:
- 接口的动态特性
- 接口类型变量兼具动态类型,即在运行时存储在接口类型变量中的值的真实类型。比如:
var i interface{} = 13
中接口变量i的动态类型为int - 接口类型变量在程序运行时可以被赋值为不同的动态类型变量,从而支持运行多态
- 接口类型变量兼具动态类型,即在运行时存储在接口类型变量中的值的真实类型。比如:
26.1 nil error值 != nil
下方的代码输出:error:
type MyError struct {
error
}
var ErrBad = MyError{
error: errors.New("bad error"),
}
func bad() bool {
return false
}
func returnsError() error {
var p *MyError = nil
if bad() {
p = &ErrBad
}
return p
}
func main() {
e := returnsError()
if e != nil {
fmt.Printf("error: %+v\n", e)
return
}
fmt.Println("ok")
}
26.2 接口类型变量的内部表示
接口类型变量在运行时的表示:
// $GOROOT/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
- eface:用于表示没有方法的空接口(empty interface)类型变量,即interface{}类型的变量
- iface:用于表示其余拥有方法的接口(interface)类型变量
共同点:都有两个指针变量,且第二个指针字段的功能相同,都指向当前赋值给该接口类型变量的动态类型变量的值
不同点:
- eface所表示空接口类型无方法列表,因此第一个指针字段指向一个_type类型结构,该结构为该接口类型变量的动态类型的信息
// $GOROOT/src/runtime/type.go
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
- 而iface除了要存储动态类型信息之外,还要存储接口本身的信息(接口的类型信息、方法列表信息等)以及动态类型所实现的方法的信息,因此iface的第一个字段指向一个itab类型结构:
// $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 // 接口方法集合切片
}
两种接口类型可以分别简记为eface(_type, data)和iface(tab, data),_type和tab可以可以统一看作动态类型的类型信息
Go语言中每种类型都有唯一的_type信息,无论是内置原生类型,还是自定义类型。Go运行时会为程序内的全部类型建立只读的共享_type信息表,因此拥有相同动态类型的同类接口变量的_type/tab信息是相同的
而接口类型变量的data部分则指向一个动态分配的内存空间,该内存空间存储的是赋值给接口类型变量的动态类型变量的值,未显式初始化的接口类型变量的值为nil,即该变量的_type/tab和data都为nil
判断两个接口类型变量是否相同,只需判断_type/tab是否相同以及data指针所指向的内存空间所存储的数据值是否相同(注意:不是data指针的值)
(1) nil接口变量
未赋值初始值的接口类型变量的值为nil
无论是空接口类型变量还是非空接口类型变量,一旦变量值为nil,那么它们内部分表示均为(0x0,0x0),即类型信息和数据信息均为空。因此上面的变量i和变量err等值判断为true
//输出
//(0x0,0x0)
//(0x0,0x0)
//i = nil, true
//err = nil, true
//i = err, true
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)
println("")
}
(2) 空接口类型变量
对于空接口类型变量,只有在_type和data所指数据内容一致(注意:不是数据指针的值一致)的情况下,两个空接口类型变量之间才能画等号
Go对data的内存分配是有优化的,虽然一般会为data重新分配内存空间,但是可能不会分配新内存空间,而是指向一块事先已经创建好的静态数据区
//输出
//eif1: (0xe62a40,0xc00009df70)
//eif2: (0xe62a40,0xc00009df68)
//eif1 = eif2: false
//eif1: (0xe62a40,0xc00009df70)
//eif2: (0xe62a40,0xe9ecb8)
//eif1 = eif2: true
//eif1: (0xe62a40,0xc00009df70)
//eif2: (0xe62b00,0xe9ecb8)
//eif1 = eif2: false
func printEmptyInterface() {
// empty接口变量
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)
eif2 = 17
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2)
eif2 = int64(17)
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2)
println("")
}
(3) 非空接口类型变量
与空接口类型变量一样,只有在tab和data所指数据内容一致的情况下,两个非空接口类型变量之间才能画等号
对于err1 = (*T)(nil)
,println输出的是err1(0xea0458,0x0),即非空接口类型变量的类型信息并不为空,数据指针为空,因此它与nil(0x0,0x0)之间不能画等号
type T int
func (t T) Error() string {
return "bad error"
}
//输出
//err1: (0xea0458,0x0)
//err2: (0x0,0x0)
//err1 = nil: false
//err1: (0xea0498,0xe9ecc0)
//err2: (0xea0498,0xe9ecc8)
//err1 = err2 (1): false
//err1: (0xea0498,0xe9ecc0)
//err2: (0xea0498,0xe9ecc0)
//err1 = err2 (2): true
//err1: (0xea0498,0xe9ecc0)
//err2: (0xea03d8,0xc00004c240)
//err1 = err2 (3): false
func printNonEmptyInterface() {
var err1 error // 非空接口类型
var err2 error // 非空接口类型
err1 = (*T)(nil)
println("err1:", err1)
println("err2:", err2)
println("err1 = nil:", err1 == nil)
err1 = T(5)
err2 = T(6)
println("err1:", err1)
println("err2:", err2)
println("err1 = err2 (1):", err1 == err2)
err1 = T(5)
err2 = T(5)
println("err1:", err1)
println("err2:", err2)
println("err1 = err2 (2):", err1 == err2)
err2 = fmt.Errorf("%d\n", 5)
println("err1:", err1)
println("err2:", err2)
println("err1 = err2 (3):", err1 == err2)
println("")
}
(4) 空接口类型变量与非空接口类型变量的等值比较
Go在进行等值比较时,类型比较使用的是eface._type和iface.tab._type,因此当数据指针都指向相同的值时,空接口类型变量与非空接口类型变量之间可以画等号
//输出
//eif: (0xe65f60,0xe9ecc0)
//err: (0xea0498,0xe9ecc0)
//eif = err: true
//eif: (0xe65f60,0xe9ecc0)
//err: (0xea0498,0xe9ecc8)
//eif = err: false
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)
}
26.3 输出接口类型变量内部表示的详细信息
通过复制runtime包eface和iface相关类型源码,可以打印出eface和iface内部的详细信息,证明空接口类型变量与非空接口类型变量可以相等
26.4 接口类型的装箱原理
装箱(boxing):把值类型转换成引用类型。Go语言中将任意类型赋值给一个接口类型变量都是装箱操作。接口类型变量的装箱操作,由Go编译器和运行时共同完成
- convT2E和convT2I两个runtime包函数
- 经过装箱之后,箱内的数据(存放在新分配的内存空间中)与原变量便无瓜葛了,除非是指针类型
- convT2E和convT2I函数的类型信息依赖Go编译器的工作,编译器知道每个要转换为接口类型变量(toType)的动态类型变量的类型(fromType),会根据这一类型选择适当的convT2X函数
- 装箱是一个有性能损耗的操作,因此Go提供了一系列快速转换函数,这些函数去除了typedmemmove操作,增加了零值快速返回
- 同时Go建立了staticbytes区域,对byte大小的值进行装箱操作时不再分配新内存,而是利用staticbytes区域的内存空间,例如bool类型