Go 语言实现——数据结构

变量(variable) = 类型(type)+变量名(name)+值(value)。

值(value)的内存结构

基础类型

+---+
| 1 |  int
+---+
+---+
|3.1|  float32
+---+
+---+---+---+---+
| 1 | 2 | 3 | 4 | [4]int
+---+---+---+---+

结构体(struct)

+---+---+---+---+---+---+---+---+
| 1 | 2 | 0 | 0 | 3 | 0 | 0 | 0 | struct{a byte; b byte; c int32} = {1,2,3}
+---+---+---+---+---+---+---+---+
  a   b           c

字符串和切片(slice)

+---------+---------+
| pointer | len=5   | s = "hello"
+---------+---------+
  |
  +---+---+---+---+---+
  | h | e | l | l | o | [5]byte
  +---+---+---+---+---+
      |
+---------+---------+
| pointer | len=2   | sub = s[1:3]
+---------+---------+

+---------+---------+---------+
| pointer | len=8   | cap=8   | x = []int{0,1,2,3,4,5,6,7}
+---------+---------+---------+
    |
    +---+---+---+---+---+---+---+---+
    | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | [8]int
    +---+---+---+---+---+---+---+---+
        |
        +---------+---------+---------+
        | pointer | len=2   | cap=5   | y = x[1:3:6]
        +---------+---------+---------+

从上面可以看出,Go 在存储变量值时和 C 比较类似,就是存的 raw 数据。

那么问题来了,既然变量值里没有保存类型指针之类的额外数据, reflect.TypeOf(v) 是怎么取得变量值的类型信息的呢?

我们看一下 reflect.TypeOf(v) 的函数原型:

func TypeOf(i interface{}) Type

从函数原型来看,答案应该就在这个 interface{} 里了 。

接口 interface 的实现

interface 是 Go 语言中类型系统的注入点。

根据 interface 是否包含有 method,interface 用 ifaceeface 这两种结构体来保存。eface 是不含 method 的 interface 结构,即 interface{} 。

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

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

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32 // copy of _type.hash. Used for type switches.
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // variable sized
}

iface/eface 中的 data 是指向实际值(value)的指针, itab._type 或者 _type 是指向类型信息的指针,Go 语言在编译的时候会将所有类型信息保存在可执行文件中,在运行时载入内存,最后在创建 interface{} 结构的时候将类型信息注入到 interface{} 结构中。 通过 s.tab->type 即可获取值(value)的类型信息。

最后,我们用一段简单的代码看看 interface{} 具体是怎么工作的。

// test.go
package main

func main() {
    s := "ABC"
    var i interface{} = s
    s1 := i.(int)
    print(s1)
}

编译并打印语法树:

$ go tool compile -W test.go

语法树比较长,下面是相关部分简化并注释后的输出

// 还有一个 before walk main 是原始的语法树,after 是 golang 对语法树做了所有修改之后的最终语法树。
after walk main
    // 声明一个 string 类型的变量 s
.   DCL
.   .   NAME-main.s string

    // AS == ASSIGMENT,将 string value "abcd" 赋值给变量 s
.   AS
.   .   NAME-main.s string
.   .   LITERAL-"abcd" string

    // 声明一个 interface{} 类型的变量 i
.   DCL
.   .   NAME-main.i INTER-interface {}

    // 将变量 s 赋值给自动生成的临时变量 autotmp_0
.   AS
.   .   NAME-main..autotmp_0 string
.   .   NAME-main.s string

.   AS-init
        // 将 autotmp_0 赋值 给 autotmp_2
.   .   AS l(5) tc(1)
.   .   .   NAME-main..autotmp_2 string
.   .   .   NAME-main..autotmp_0 string
.   AS
        // 将(string类型指针,autotmp_2)赋值给变量 i
.   .   NAME-main.i INTER-interface {}
.   .   EFACE u(2) l(5) tc(1) INTER-interface {}
.   .   .   ADDR PTR64-*uint8
.   .   .   .   NAME-type.string uint8
.   .   .   ADDR PTR64-*string
.   .   .   .   NAME-main..autotmp_2 string

.   VARKILL
.   .   NAME-main..autotmp_0 string

    // 声明一个新的变量 s1
.   DCL l(6)
.   .   NAME-main.s1 int

.   AS
.   .   NAME-main..autotmp_1 int

    // 检查 i 的类型是不是 int,是的话赋值给 autotmp_1
.   AS
.   .   NAME-main..autotmp_1 int
.   .   DOTTYPE
.   .   .   NAME-main.i INTER-interface {}

    // 将 autotmp_1 赋值给 s1
.   AS
.   .   NAME-main.s1 int
.   .   NAME-main..autotmp_1 int

.   VARKILL l(6) tc(1)
.   .   NAME-main..autotmp_1 int

参考:

uintptr 和 unsafe.Pointer 的区别

两种类型的定义如下:

// $GOROOT/src/builtin/builtin.go
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

// $GOROOT/src/unsafe/unsafe.go
type Pointer *ArbitraryType

uintptr 它就是一个 整型 类型,这个类型的比特位数(bit size)足够大,可以存储指针(内存地址)而不溢出。uintptr 中的内容就是一个整数,这个整数和其它整数没有区别,只不过这个整数是一个指针(内存地址),gc 对 uintptr 是无感知的,所以可能 uintptr 变量还在,但它指向的对象已经被 gc 了。

而 unsafe.Pointer 是一个可以指向任意类型对象的指针,unsafe.Pointer 在,他所指向的对象就一定在,不会被 gc 掉。

看一个 uintptr 的应用场景:Go 运行时中有一个 noescape 函数用来切断 逃逸分析 系统的数据流跟踪,避免传入的指针逃逸。

// $GOROOT/src/runtime/stubs.go
func noescape(p unsafe.Pointer) unsafe.Pointer {
    x := uintptr(p)
    return unsafe.Pointer(x ^ 0) // 任何数值与 0 异或都是原数
}

这个函数将传入的指针转换成 uintptr 类型,也就一个整数数值,然后将这个数值异或 0 之后(还是原来的数值)再转换会指针返回。传入的指针和返回的指针都是指向同一个地址,但是经过一次 uintptr 的转换,这两个指针解耦合了。