go var () 什么意思_Go 当中 interface 设计(中)

原文链接:

Go 当中 interface 设计(中)​mp.weixin.qq.com

eface vs iface

efaceiface 都是 Go 中描述接口的底层数据结构, 区别在于 iface 描述的接口包含方法, 而 eface 则是不包含 任何方法的空接口: interface{}.

接口的底层数据结构:

// 空接口, interface{} 的底层结构
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

// 方法接口, 自定义的接口的数据结构
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter *interfacetype // 接口类型
    _type *_type         // 值类型
    hash  uint32         // 值类型 _type 当中的 hash 的拷贝
    _     [4]byte
    fun   [1]uintptr
}

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

type name struct {
    bytes *byte
}
type imethod struct {
    name int32
    ityp int32
}

type _type struct {
    size       uintptr
    ptrdata    uintptr // 包含所有指针的内存前缀的大小. 如果为0, 表示的是一个值, 而非指针
    hash       uint32
    tflag      uint8
    align      uint8
    fieldalign uint8
    kind       uint8
    alg        uintptr
    gcdata     *byte
    str        int32
    ptrToThis  int32
}

iface 维护两个指针, tab 指向一个 itab 实体, 它表示接口的类型 以及赋值给这个接口的实体类型. data 则指向接口 具体的值, 一般情况下是一个指向堆内存的指针.

itab 结构: inter 字段描述了接口的类型(静态), _type 字段描述了接口值的类型(动态), 包括内存对齐方式, 大小等. fun 字段放置接口类型的方法地址 (与接口方法对于的具体数据类型的方法地址??), 实现接口调用方法的动态分派.

为何 fun 数组的大小为1, 如果接口定义了多个方法怎么办? 实际上, 这里存储的是第一个方法的函数指针, 如果有更多的方法, 在 它之后的内存空间里继续存在. 从汇编角度来看, 通过增加地址就能获取到这些函数指针, 没什么影响. 方法是按照函数名称的字典顺序 进行排列的.

iterfacetype 类型, 它描述了接口的类型(静态). 它包装了 _type 类型, _type 是描述 Go 语言各种数据类型的结构体. mhdr 字段, 表示接口定义的函数列表, 通过这里的内容, 在反射的时候可以获取实际函数的指针. pkgpath 描述了接口的包名.

0ab5da98f92df3abde208e80449a3790.png
iface 结构

相比 iface, eface 就比较简单, 只维护了一个 _type 字段, 表示空接口所承载的具体的实体类型. data 描述了其值.

802cebaf00dccd13a74604c4198a378b.png
eface 结构

接口的动态类型和动态值

接口类型和 nil 做比较:

接口值的零值指 动态类型动态值 都为 nil. 当且仅当这两部分的值都为 nil 的状况下, 这个接口值才被认为是nil, 即 接口值 == nil.

例子1:

type Coder interface {
    code()
}

type Gopher struct {
    name string
}

func (g Gopher) code() {
    fmt.Printf("%s is codingn", g.name)
}

func main() {
    var c Coder
    fmt.Println(c == nil) // true
    fmt.Printf("c: %T, %vn", c, c)

    var g *Gopher
    fmt.Println(g == nil) // true

    c = g
    fmt.Println(c == nil) // fasle, 值类型为 *Gopher, 值是 nil
    fmt.Printf("c: %T, %vn", c, c)
}

例子2:

type MyError struct {}

func (i MyError) Error() string {
    return "MyError"
}

func Process() error {
    var err *MyError = nil
    return err // 值类型为 *MyError, 值是 nil
}

func main() {
    err := Process()
    fmt.Println(err) // nil

    fmt.Println(err == nil) // false
}

例子3:

type iface struct {
    itab, data uintptr
}

func main() {
    var a interface{} = nil

    /* 
     * 等价形式:
     * var t *int
     * var b interface{} = t
     */
    var b interface{} = (*int)(nil) 

    x := 5
    var c interface{} = (*int)(&x)

    ia := *(*iface)(unsafe.Pointer(&a)) // 值类型为 nil, 值为 nil
    ib := *(*iface)(unsafe.Pointer(&b)) // 值类型为 *int, 值为 nil
    ic := *(*iface)(unsafe.Pointer(&c)) // 值类型为 *int, 值为 5

    fmt.Println(ia, ib, ic) 

    fmt.Println(*(*int)(unsafe.Pointer(ic.data)))
}

编译器自动检测类型是否实现接口

奇怪的代码:

var _ io.Writer = (*myWriter)(nil)

为啥有上面的代码, 上面的代码究竟是要干什么? 编译器会通过上述的代码检查 *myWriter 类型是否实现了 io.Writer 接口.

例子:

type myWriter struct{
}

/*
func (w myWriter) Write(p []byte) (n int, err error) {
    return
}
*/

func main() {
    // 检查 *myWriter 是否实现了 io.Writer 接口
    var _ io.Writer = (*myWriter)(nil)

    // 检查 myWriter 是否实现了 io.Writer 接口
    var _ io.Writer = myWriter{}
}
注释掉为 myWriter 定义的 Writer 函数后, 运行程序, 报错信息: *myWriter/myWriter 未实现 io.Writer 接口, 也 就是未实现 Write 方法. 解除注释后, 运行程序不报错.

实际上, 上述赋值语句会发生隐式转换, 在转换的过程中, 编译器会检测等号右边的类型是否实现了等号左边接口所规定的函数.

接口的构造过程

先从一个例子开始:

type Person interface {
    grow()
}

type Student struct {
    age int
    name string
}

func (p Student) grow() {
    p.age += 1
    return
}

func main() {
    var qcrao = Person(Student{age: 18, name:"san"})

    fmt.Println(qcrao)
}

使用 go tool compile -S example.go 打印汇编代码. go 版本是 1.13

"".main STEXT size=331 args=0x0 locals=0xa8
    0x0000 00000 (example.go:19)    TEXT    "".main(SB), ABIInternal, $168-0
    0x0000 00000 (example.go:19)    MOVQ    (TLS), CX
    0x0009 00009 (example.go:19)    LEAQ    -40(SP), AX
    0x000e 00014 (example.go:19)    CMPQ    AX, 16(CX)
    0x0012 00018 (example.go:19)    JLS 321
    0x0018 00024 (example.go:19)    SUBQ    $168, SP
    0x001f 00031 (example.go:19)    MOVQ    BP, 160(SP)
    0x0027 00039 (example.go:19)    LEAQ    160(SP), BP
    0x002f 00047 (example.go:20)    XORPS   X0, X0
    0x0032 00050 (example.go:20)    MOVUPS  X0, ""..autotmp_1+136(SP)
    0x003a 00058 (example.go:20)    MOVQ    $0, ""..autotmp_1+152(SP)
    0x0046 00070 (example.go:20)    MOVQ    $18, ""..autotmp_1+136(SP)
    0x0052 00082 (example.go:20)    LEAQ    go.string."san"(SB), AX
    0x0059 00089 (example.go:20)    MOVQ    AX, ""..autotmp_1+144(SP)
    0x0061 00097 (example.go:20)    MOVQ    $3, ""..autotmp_1+152(SP)
    0x006d 00109 (example.go:20)    LEAQ    go.itab."".Student,"".Person(SB), AX
    0x0074 00116 (example.go:20)    MOVQ    AX, (SP)
    0x0078 00120 (example.go:20)    LEAQ    ""..autotmp_1+136(SP), AX
    0x0080 00128 (example.go:20)    MOVQ    AX, 8(SP)
    0x0085 00133 (example.go:20)    CALL    runtime.convT2I(SB)
    0x008a 00138 (example.go:20)    MOVQ    24(SP), AX
    0x008f 00143 (example.go:20)    MOVQ    16(SP), CX
    0x0094 00148 (example.go:20)    MOVQ    CX, "".qcrao+64(SP)
    0x0099 00153 (example.go:20)    MOVQ    AX, "".qcrao+72(SP)
    0x009e 00158 (example.go:22)    MOVQ    "".qcrao+72(SP), AX
    0x00a3 00163 (example.go:22)    MOVQ    "".qcrao+64(SP), CX
    0x00a8 00168 (example.go:22)    MOVQ    CX, ""..autotmp_3+80(SP)
    0x00ad 00173 (example.go:22)    MOVQ    AX, ""..autotmp_3+88(SP)
    0x00b2 00178 (example.go:22)    MOVQ    CX, ""..autotmp_4+56(SP)
    0x00b7 00183 (example.go:22)    CMPQ    ""..autotmp_4+56(SP), $0
    0x00bd 00189 (example.go:22)    JNE 193
    0x00bf 00191 (example.go:22)    JMP 319
    0x00c1 00193 (example.go:22)    TESTB   AL, (CX)
    0x00c3 00195 (example.go:22)    MOVQ    8(CX), AX
    0x00c7 00199 (example.go:22)    MOVQ    AX, ""..autotmp_4+56(SP)
    0x00cc 00204 (example.go:22)    JMP 206
    0x00ce 00206 (example.go:22)    PCDATA  $1, $5
    0x00ce 00206 (example.go:22)    XORPS   X0, X0
    0x00d1 00209 (example.go:22)    MOVUPS  X0, ""..autotmp_2+96(SP)
    0x00d6 00214 (example.go:22)    LEAQ    ""..autotmp_2+96(SP), AX
    0x00db 00219 (example.go:22)    MOVQ    AX, ""..autotmp_6+48(SP)
    0x00e0 00224 (example.go:22)    TESTB   AL, (AX)
    0x00e2 00226 (example.go:22)    MOVQ    ""..autotmp_4+56(SP), CX
    0x00e7 00231 (example.go:22)    MOVQ    ""..autotmp_3+88(SP), DX
    0x00ec 00236 (example.go:22)    MOVQ    CX, ""..autotmp_2+96(SP)
    0x00f1 00241 (example.go:22)    MOVQ    DX, ""..autotmp_2+104(SP)
    0x00f6 00246 (example.go:22)    TESTB   AL, (AX)
    0x00f8 00248 (example.go:22)    JMP 250
    0x00fa 00250 (example.go:22)    MOVQ    AX, ""..autotmp_5+112(SP)
    0x00ff 00255 (example.go:22)    MOVQ    $1, ""..autotmp_5+120(SP)
    0x0108 00264 (example.go:22)    MOVQ    $1, ""..autotmp_5+128(SP)
    0x0114 00276 (example.go:22)    MOVQ    AX, (SP)
    0x0118 00280 (example.go:22)    MOVQ    $1, 8(SP)
    0x0121 00289 (example.go:22)    MOVQ    $1, 16(SP)
    0x012a 00298 (example.go:22)    CALL    fmt.Println(SB)
    0x012f 00303 (example.go:23)    MOVQ    160(SP), BP
    0x0137 00311 (example.go:23)    ADDQ    $168, SP
    0x013e 00318 (example.go:23)    RET
    0x013f 00319 (example.go:22)    JMP 206
    0x0141 00321 (example.go:22)    NOP
    0x0141 00321 (example.go:19)    CALL    runtime.morestack_noctxt(SB)
    0x0146 00326 (example.go:19)    JMP 0

最重要核心的代码是 runtime.convT2I(SB), 该函数位于 src/runtime/iface.go 当中. convT2I() 函数是将一个 struct 对象转换为 iface. 类似的方法还有, convT64(), convTstring() 等.这些方法都涉及到了接口的动态值.

下面看一下其中几个代表性的函数源码:

// 源码位置 src/runtime/iface.go
var (
    uint64Eface interface{} = uint64InterfacePtr(0)
    stringEface interface{} = stringInterfacePtr("")
    sliceEface  interface{} = sliceInterfacePtr(nil)

    uint64Type *_type = (*eface)(unsafe.Pointer(&uint64Eface))._type
    stringType *_type = (*eface)(unsafe.Pointer(&stringEface))._type
    sliceType  *_type = (*eface)(unsafe.Pointer(&sliceEface))._type
)

// type(uint64) -> interface
func convT64(val uint64) (x unsafe.Pointer) {
    if val == 0 {
        x = unsafe.Pointer(&zeroVal[0])
    } else {
        x = mallocgc(8, uint64Type, false)
        *(*uint64)(x) = val
    }
    return
}

// type(uint64) -> interface
func convTstring(val string) (x unsafe.Pointer) {
    if val == "" {
        x = unsafe.Pointer(&zeroVal[0])
    } else {
        x = mallocgc(unsafe.Sizeof(val), stringType, true)
        *(*string)(x) = val
    }
    return
}

// type -> iface 
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type

    // 启用了 -race 选项
    if raceenabled {
        raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
    }
    // 启用了 -msan 选项
    if msanenabled {
        msanread(elem, t.size)
    }

    // 生成 itab 当中动态类型的内存空间(指针), 并将 elem 的值拷贝相应位置
    x := mallocgc(t.size, t, true) // convT2I 和 convT2Enoptr 的区别
    typedmemmove(t, x, elem)
    i.tab = tab
    i.data = x
    return
}
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer
分配一个 size 大小字节的 object. 从per-P缓存的空闲列表中分配小对象; 从堆直接分配大对象( >32 kB), 参数 needzero 描述分配的对象是否是指针 参数 typ 描述当前分配对象的数据类型

上述的代码逻辑都比较简单, 这里就不再解释了.

参考:

  • 深度解密Go语言之关于 interface 的 10 个问题
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值