Loading

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类型
posted @ 2024-02-14 13:42  brynchen  阅读(15)  评论(0编辑  收藏  举报