关于golang:Go进阶基础特性接口

59次阅读

共计 6689 个字符,预计需要花费 17 分钟才能阅读完成。

接口定义了一种标准,形容了类的行为和性能。咱们都晓得,Go 语言中的接口是所谓的 Duck Typing,实现接口的所有办法也就隐式地实现了接口,那么,它是怎么实现的呢?

数据结构

在 Go 语言中,接口分为两类:

  • eface:用于示意没有办法的空接口类型变量,即 interface{} 类型的变量。
  • iface:用于示意其余领有办法的接口类型变量。

eface

eface 的数据结构如下:

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

eface 有两个属性,别离是 _type 和 data,别离指向接口变量的动静类型和动静值。

再进一步看看 type 属性的构造:

type _type struct {
    size       uintptr // 类型大小
    ptrdata    uintptr // 蕴含所有指针的内存前缀的大小
    hash       uint32  // 类型的 hash 值
    tflag      tflag   // 类型的 flag 标记,次要用于反射
    align      uint8   // 内存对齐相干
    fieldAlign uint8   // 内存对齐相干
    kind       uint8   // 类型的编号,蕴含 Go 语言中的所有类型,如 kindBool、kindInt 等
    equal func(unsafe.Pointer, unsafe.Pointer) bool // 用于比拟此对象的回调函数
    gcdata    *byte    // 存储垃圾收集器的 GC 类型数据
    str       nameOff 
    ptrToThis typeOff
}

注:Go 语言的各种数据类型都是在 _type 字段的根底上,减少一些额定的字段来进行治理的。

来看一个 eface 变量的例子:

type T struct {
    n int
    s string
}

func main() {
    var t = T {
        n: 17,
        s: "hello, interface",
    }
    var ei interface{} = t
    println(ei)
}           
            

ei 变量的构造对应于下图:

iface

iface 的构造如下:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

与 eface 构造体一样,iface 存储的也是类型和值信息,不过因为 iface 还要存储接口自身的信息以及动静类型所实现的办法的信息,因而 iface 稍显简单,它的第一个字段指向一个 itab 类型构造:

type itab struct {
    inter *interfacetype // 接口的类型信息
    _type *_type         // 动静类型信息
    hash  uint32         // _type.hash 的正本,当咱们想将 interface 类型转换成具体类型时,能够应用该字段疾速判断指标类型和具体类型 _type 是否统一
    _     [4]byte    
    fun   [1]uintptr     // 存储接口办法集的具体实现的地址,蕴含一组函数指针,实现了接口办法的动静分派,且每次在接口产生变更时都会更新
}

进一步开展 interfacetype 构造体。源码如下:

type nameOff int32
type typeOff int32

type imethod struct {
    name nameOff
    ityp typeOff
}

type interfacetype struct {
    typ     _type     // 动静类型信息
    pkgpath name      // 包名信息
    mhdr    []imethod // 接口所定义的办法列表}

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
    println(i)
}            

变量 i 对应如下:

值接收者和指针接收者

在应用 Go 语言的过程中,在调用办法的时候,不论办法的接收者是什么类型,该类型的值和指针都能够调用,不用严格合乎接收者的类型。

须要记住的一点是:在 Go 语言中,如果实现了接收者是值类型的办法,会隐含实现接收者是指针类型的办法,反之则不成立。之所以能够应用值类型调用指针类型的办法,是语法糖的作用。如果只有指针类型实现了接口,应用值类型调用接口办法则会报错。

接口值的比拟

咱们看到,所有的接口类型其实底层都蕴含两个字段:类型和值,也被称为动静类型和动静值。因而接口值包含动静类型和动静值,在比拟接口值的时候,咱们须要别离对接口值的类型和值进行比拟。

nil 接口变量

package main

func main() {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("")
}

// 输入后果

(0x0,0x0)
(0x0,0x0)
i = nil: true
err = nil: true
i = err: true

咱们看到,无论是空接口类型变量还是非空接口类型变量,一旦变量值为 nil,那么它们外部示意均为 (0x0,0x0),即类型信息和数据信息均为空。因而下面的变量 i 和 err 等值判断为 true。

空接口类型变量

func main() {var eif1 interface{}
    var eif2 interface{}
    n, m := 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)
}

// 输入后果

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

从输入后果能够看到:对于空接口类型变量,只有在 _type 和 data 所指数据内容统一(不是数据指针的值统一)的状况下,两个空接口类型变量才相等。

Go 在创立 eface 时个别会为 data 重新分配内存空间,将动静类型变量的值复制到这块内存空间,并将 data 指针指向这块内存空间。因而咱们在少数状况下看到的 data 指针值是不同的。但 Go 对于 data 的调配是有优化的,也不是每次都调配新内存空间,就像下面的 eif2 的 0x10eb3d0 和 0x10eb3d8 两个 data 指针值,显然是间接指向了一块当时创立好的静态数据区。

非空接口类型变量

func main() {
    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            

与空接口类型变量一样,只有在 tab 和 data 所指数据内容统一的状况下,两个非空接口类型变量之间能力画等号。

空接口类型变量与非空接口类型变量

func main() {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                 

空接口类型变量和非空接口类型变量外部示意的构造有所不同,仿佛肯定不能相等。但 Go 在进行等值比拟时,类型比拟应用的是 eface 的 _type 和 iface 的 tab._type,因而就像咱们在这个例子中看到的那样,当 eif 和 err 都被赋值为 T(5) 时,两者之间是相等的。

类型转换

惯例变量转换接口变量

先看代码示例:

import "fmt"

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)
}

应用 go tool compile -S 命令查看生成的汇编代码,能够看到这两个转换过程对应了 runtime 包的两个函数:

......
0x0050 00080 (main.go:24)       CALL    runtime.convT2E(SB)
......
0x0089 00137 (main.go:27)       CALL    runtime.convT2I(SB)
......

这两个函数的源码如下:

// $GOROOT/src/runtime/iface.go
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
    if raceenabled {raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
    }
    if msanenabled {msanread(elem, t.size)
    }
    x := mallocgc(t.size, t, true)
    typedmemmove(t, x, elem)
    e._type = t
    e.data = x
    return
}

func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
    if raceenabled {raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
    }
    if msanenabled {msanread(elem, t.size)
    }
    x := mallocgc(t.size, t, true)
    typedmemmove(t, x, elem)
    i.tab = tab
    i.data = x
    return
}                        

convT2E 用于将任意类型转换为一个 eface,convT2I 用于将任意类型转换为一个 iface。两个函数的实现逻辑类似,次要思路就是依据传入的类型信息(convT2E 的 _type 和 convT2I 的 tab._type)调配一块内存空间,并将 elem 指向的数据复制到这块内存空间中,最初传入的类型信息作为返回值构造中的类型信息,返回值构造中的数据指针指向新调配的那块内存空间。

那么 convT2E 和 convT2I 函数的类型信息从何而来?这些都依赖 Go 编译器的工作。Go 也在一直转换操作进行优化,包含对常见类型(如整型、字符串、切片等)提供一系列疾速转换函数:

// $GOROOT/src/cmd/compile/internal/gc/builtin/runtime.go
func convT16(val any) unsafe.Pointer     // val 必须是一个 uint-16 相干类型的参数
func convT32(val any) unsafe.Pointer     // val 必须是一个 unit-32 相干类型的参数
func convT64(val any) unsafe.Pointer     // val 必须是一个 unit-64 相干类型的参数
func convTstring(val any) unsafe.Pointer // val 必须是一个字符串类型的参数
func convTslice(val any) unsafe.Pointer  // val 必须是一个切片类型的参数                        

编译器晓得每个要转换为接口类型变量的动静类型变量的类型,会依据这一类型抉择适当的 convT2X 函数。

接口变量相互转换

接口之间相互转换的前提是类型兼容,也就是都实现了接口定义的办法。上面咱们来看一下运行时转换接口类型的办法:

func convI2I(inter *interfacetype, i iface) (r iface) {
    tab := i.tab
    if tab == nil {return}
    if tab.inter == inter {
        r.tab = tab
        r.data = i.data
        return
    }
    r.tab = getitab(inter, tab._type, false)
    r.data = i.data
    return
}

代码比较简单,函数参数 inter 示意接口类型,i 示意绑定了动静类型的接口变量,返回值 r 就是须要转换的新的 iface。通过后面的剖析,咱们晓得 iface 是由 tab 和 data 两个字段组成。所以,convI2I 函数真正要做的事就是找到并设置好新 iface 的 tab 和 data,就功败垂成了。

咱们还晓得,tab 是由接口类型 interfacetype 和 实体类型 _type 组成的。所以最要害的语句是 r.tab = getitab(inter, tab._type, false),来看一下 getitab 的外围代码:

func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    var m *itab

    t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
    if m = t.find(inter, typ); m != nil {goto finish}

    lock(&itabLock)
    if m = itabTable.find(inter, typ); m != nil {unlock(&itabLock)
        goto finish
    }

    m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
    m.inter = inter
    m._type = typ

    m.hash = 0
    m.init()
    itabAdd(m)
    unlock(&itabLock)
finish:
    if m.fun[0] != 0 {return m}
    if canfail {return nil}

    panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}
  • 调用 atomic.Loadp 办法加载并查找现有的 itab hash table,看看是否是否能够找到所需的 itab 元素。
  • 若没有找到,则调用 lock 办法对 itabLock 上锁,并再查找一次。

    • 若找到,则跳到 finish 标识的收尾步骤。
    • 若没有找到,则新生成一个 itab 元素,并调用 itabAdd 办法新增到全局的 hash table 中。
  • 返回所需的 itab。

正文完
 0