2

An interface defines a specification that describes the behavior and functionality of a class. We all know that the interface in the Go language is the so-called Duck Typing, and all methods that implement the interface implicitly implement the interface, so how is it implemented?

data structure

In the Go language, interfaces are divided into two categories:

  • eface: used to represent an empty interface type variable without methods, that is, a variable of type interface{}.
  • iface: used to represent the rest of the interface type variables that have methods.

eface

The data structure of eface is as follows:

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

eface has two attributes, namely _type and data, which respectively point to the dynamic type and dynamic value of the interface variable.

Take a closer look at the structure of the type attribute:

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
}

Note: Various data types in Go language are based on the _type field and add some additional fields for management.

Let's see an example of an eface variable:

type T struct {
    n int
    s string
}

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

The structure of the ei variable corresponds to the following figure:

image.png

iface

The structure of iface is as follows:

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

Like the eface structure, iface stores type and value information, but because iface also stores information about the interface itself and the methods implemented by dynamic types, iface is slightly more complicated, its first field points to an itab Type structure:

type itab struct {
    inter *interfacetype // 接口的类型信息
    _type *_type         // 动态类型信息
    hash  uint32         // _type.hash 的副本,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 _type 是否一致
    _     [4]byte    
    fun   [1]uintptr     // 存储接口方法集的具体实现的地址,包含一组函数指针,实现了接口方法的动态分派,且每次在接口发生变更时都会更新
}

Expand the interfacetype structure further. The source code is as follows:

type nameOff int32
type typeOff int32

type imethod struct {
    name nameOff
    ityp typeOff
}

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

An example of iface is as follows:

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

The variable i corresponds to the following:

image.png

value receiver and pointer receiver

In the process of using the Go language, when calling a method, regardless of the type of the receiver of the method, the value and pointer of the type can be called, and it does not have to strictly conform to the type of the receiver.

One thing to remember is that in Go, if you implement a method whose receiver is a value type, it will implicitly implement a method whose receiver is a pointer type, but not vice versa. The reason why you can use the value type to call the method of the pointer type is the syntactic sugar. If only the pointer type implements the interface, calling an interface method with a value type will throw an error.

interface value comparison

We see that all interface types actually contain two fields at the bottom: type and value, also known as dynamic type and dynamic value. Therefore, interface values include dynamic types and dynamic values. When comparing interface values, we need to compare the types and values of interface values respectively.

nil interface variable

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

We see that, whether it is an empty interface type variable or a non-empty interface type variable, once the variable value is nil, then their internal representation is (0x0, 0x0), that is, the type information and data information are empty. Therefore, the above variables i and err are judged to be true.

Null interface type variable

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

It can be seen from the output result: for the empty interface type variable, the two empty interface type variables are equal only when the data contents pointed to by _type and data are the same (not the value of the data pointer).

When Go creates an eface, it generally reallocates the memory space for data, copies the value of the dynamic type variable to this memory space, and points the data pointer to this memory space. So the data pointer value we see is different in most cases. But Go optimizes the allocation of data, and does not allocate new memory space every time, just like the two data pointer values 0x10eb3d0 and 0x10eb3d8 of eif2 above, obviously point directly to a pre-created static data area.

non-null interface type variable

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            

As with the empty interface type variable, the equal sign can be drawn between two non-empty interface type variables only if the data contents pointed to by tab and data are consistent.

Empty interface type variables and non-empty interface type variables

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                 

The structure represented internally by a variable of an empty interface type and a variable of a non-empty interface type is different, and it seems that they must not be equal. But when Go does an equality comparison, the type comparison uses eface's _type and iface's tab._type, so as we can see in this example, when both eif and err are assigned T(5) , the two are equal.

type conversion

Conventional variable conversion interface variable

Look at the code example first:

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

Use the go tool compile -S command to view the generated assembly code, and you can see that the two conversion processes correspond to the two functions of the runtime package:

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

The source code of these two functions is as follows:

// $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 is used to convert any type to an eface, and convT2I is used to convert any type to an iface. The implementation logic of the two functions is similar. The main idea is to allocate a memory space according to the incoming type information (_type of convT2E and tab._type of convT2I), copy the data pointed to by elem into this memory space, and finally pass The entered type information is used as the type information in the return value structure, and the data pointer in the return value structure points to the newly allocated memory space.

So where does the type information for the convT2E and convT2I functions come from? These all rely on the work of the Go compiler. Go is also continuously optimizing conversion operations, including a series of fast conversion functions for common types (such as integers, strings, slices, etc.):

// $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必须是一个切片类型的参数                        

The compiler knows the type of each dynamically typed variable to be converted to an interface type variable and selects the appropriate convT2X function based on that type.

interface variables are converted to each other

The premise of mutual conversion between interfaces is that the types are compatible, that is, they all implement the methods defined by the interfaces. Let's take a look at how to convert interface types at runtime:

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
}

The code is relatively simple, the function parameter inter represents the interface type, i represents the interface variable bound to the dynamic type, and the return value r is the new iface that needs to be converted. Through the previous analysis, we know that iface is composed of two fields, tab and data. So, what the convI2I function really has to do is to find and set the tab and data of the new iface, and you're done.

We also know that tab is composed of interface type interfacetype and entity type _type. So the most critical statement is r.tab = getitab(inter, tab._type, false) , let's take a look at the core code of 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()})
}
  • Call the atomic.Loadp method to load and search the existing itab hash table to see if the desired itab element can be found.
  • If not found, call the lock method to lock the itabLock, and search again.

    • If found, skip to the final step identified by finish.
    • If it is not found, an itab element is newly generated, and the itabAdd method is called to add it to the global hash table.
  • Return the desired itab.

与昊
222 声望634 粉丝

IT民工,主要从事web方向,喜欢研究技术和投资之道