Go语言精进之路读书笔记第26条——了解接口类型变量的内部表示

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类型

相关推荐