zoukankan      html  css  js  c++  java
  • golang: 常用数据类型底层结构分析

    虽然golang是用C实现的,并且被称为下一代的C语言,但是golang跟C的差别还是很大的。它定义了一套很丰富的数据类型及数据结构,这些类型和结构或者是直接映射为C的数据类型,或者是用C struct来实现。了解golang的数据类型和数据结构的底层实现,将有助于我们更好的理解golang并写出质量更好的代码。

    基础类型

    源码在:$GOROOT/src/pkg/runtime/runtime.h 。我们先来看下基础类型:

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    /*

     * basic types

     */

    typedef signed char             int8;

    typedef unsigned char           uint8;

    typedef signed short            int16;

    typedef unsigned short          uint16;

    typedef signed int              int32;

    typedef unsigned int            uint32;

    typedef signed long long int    int64;

    typedef unsigned long long int  uint64;

    typedef float                   float32;

    typedef double                  float64;

     

    #ifdef _64BIT

    typedef uint64          uintptr;

    typedef int64           intptr;

    typedef int64           intgo; // Go's int

    typedef uint64          uintgo; // Go's uint

    #else

    typedef uint32          uintptr;

    typedef int32           intptr;

    typedef int32           intgo; // Go's int

    typedef uint32          uintgo; // Go's uint

    #endif

     

    /*

     * defined types

     */

    typedef uint8           bool;

    typedef uint8           byte;

    int8、uint8、int16、uint16、int32、uint32、int64、uint64、float32、float64分别对应于C的类型,这个只要有C基础就很容易看得出来。uintptr和intptr是无符号和有符号的指针类型,并且确保在64位平台上是8个字节,在32位平台上是4个字节,uintptr主要用于golang中的指针运算。而intgo和uintgo之所以不命名为int和uint,是因为int在C中是类型名,想必uintgo是为了跟intgo的命名对应吧。intgo和uintgo对应golang中的int和uint。从定义可以看出int和uint是可变大小类型的,在64位平台上占8个字节,在32位平台上占4个字节。所以如果有明确的要求,应该选择int32、int64或uint32、uint64。byte类型的底层类型是uint8。可以看下测试:

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    package main

     

    import (

            "fmt"

            "reflect"

    )

     

    func main() {

            var b byte = 'D'

            fmt.Printf("output: %v ", reflect.TypeOf(b).Kind())

    }

    ?

    1

    2

    3

    4

    $ cd $GOPATH/src/basictype_test

    $ go build

    $ ./basictype_test

    output: uint8

    数据类型分为静态类型和底层类型,相对于以上代码中的变量b来说,byte是它的静态类型,uint8是它的底层类型。这点很重要,以后经常会用到这个概念。

    rune类型

    rune是int32的别名,用于表示unicode字符。通常在处理中文的时候需要用到它,当然也可以用range关键字。

    string类型

    string类型的底层是一个C struct。

    ?

    1

    2

    3

    4

    5

    struct String

    {

            byte*   str;

            intgo   len;

    };

    成员str为字符数组,len为字符数组长度。golang的字符串是不可变类型,对string类型的变量初始化意味着会对底层结构的初始化。至于为什么str用byte类型而不用rune类型,这是因为golang的for循环对字符串的遍历是基于字节的,如果有必要,可以转成rune切片或使用range来迭代。我们来看个例子:

    $GOPATH/src

    ----basictype_test

    --------main.go

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    package main

     

    import (

        "fmt"

        "unsafe"

    )

     

    func main() {

        var str string = "hi, 陈一回~"

        p := (*struct {

            str uintptr

            len int

        })(unsafe.Pointer(&str))

     

        fmt.Printf("%+v ", p)

    }

    ?

    1

    2

    3

    4

    $ cd $GOPATH/src/basictype_test

    $ go build

    $ ./basictype_test

    output: &{str:135100456 len:14}

    内建函数len对string类型的操作是直接从底层结构中取出len值,而不需要额外的操作,当然在初始化时必需同时初始化len的值。

    slice类型

    slice类型的底层同样是一个C struct。

    ?

    1

    2

    3

    4

    5

    6

    struct  Slice

    {               // must not move anything

        byte*   array;      // actual data

        uintgo  len;        // number of elements

        uintgo  cap;        // allocated number of elements

    };

    包括三个成员。array为底层数组,len为实际存放的个数,cap为总容量。使用内建函数make对slice进行初始化,也可以类似于数组的方式进行初始化。当使用make函数来对slice进行初始化时,第一个参数为切片类型,第二个参数为len,第三个参数可选,如果不传入,则cap等于len。通常传入cap参数来预先分配大小的slice,避免频繁重新分配内存。

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    package main

     

    import (

        "fmt"

        "unsafe"

    )

     

    func main() {

        var slice []int32 = make([]int32, 5, 10)

        p := (*struct {

            array uintptr

            len   int

            cap   int

        })(unsafe.Pointer(&slice))

     

        fmt.Printf("output: %+v ", p)

    }

    ?

    1

    2

    3

    4

    $ cd $GOPATH/src/basictype_test

    $ go build

    $ ./basictype_test

    output: &{array:406958176 len:5 cap:10}

    由于切片指向一个底层数组,并且可以通过切片语法直接从数组生成切片,所以需要了解切片和数组的关系,否则可能就会不知不觉的写出有bug的代码。比如有如下代码:

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    package main

     

    import (

        "fmt"

    )

     

    func main() {

        var array = [...]int32{1, 2, 3, 4, 5}

        var slice = array[2:4]

        fmt.Printf("改变slice之前: array=%+v, slice=%+v ", array, slice)

        slice[0] = 234

        fmt.Printf("改变slice之后: array=%+v, slice=%+v ", array, slice)

    }

    ?

    1

    2

    3

    4

    5

    $ cd $GOPATH/src/basictype_test

    $ go build

    $ ./basictype_test

    改变slice之前: array=[1 2 3 4 5], slice=[3 4]

    改变slice之后: array=[1 2 234 4 5], slice=[234 4]

    您可以清楚的看到,在改变slice后,array也被改变了。这是因为slice通过数组创建的切片指向这个数组,也就是说这个slice的底层数组就是这个array。因此很显然,slice的改变其实就是改变它的底层数组。当然如果删除或添加元素,那么len也会变化,cap可能会变化。

    那这个slice是如何指向array呢?slice的底层数组指针指向array中索引为2的元素(因为切片是通过array[2:4]来生成的),len记录元素个数,而cap则等于len。

    之所以说cap可能会变,是因为cap表示总容量,添加或删除操作不一定会使总容量发生变化。我们接着再来看另一个例子:

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    package main

     

    import (

        "fmt"

    )

     

    func main() {

        var array = [...]int32{1, 2, 3, 4, 5}

        var slice = array[2:4]

        slice = append(slice, 6, 7, 8)

        fmt.Printf("改变slice之前: array=%+v, slice=%+v ", array, slice)

        slice[0] = 234

        fmt.Printf("改变slice之后: array=%+v, slice=%+v ", array, slice)

    }

    ?

    1

    2

    3

    4

    5

    $ cd $GOPATH/src/basictype_test

    $ go build

    $ ./basictype_test

    改变slice之前: array=[1 2 3 4 5], slice=[3 4 6 7 8]

    改变slice之后: array=[1 2 3 4 5], slice=[234 4 6 7 8]

    经过append操作之后,对slice的修改并未影响到array。原因在于append的操作令slice重新分配底层数组,所以此时slice的底层数组不再指向前面定义的array。

    但是很显然,这种规则对从切片生成的切片也是同样的,请看代码:

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    package main

     

    import (

        "fmt"

    )

     

    func main() {

        var slice1 = []int32{1, 2, 3, 4, 5}

        var slice2 = slice1[2:4]

        fmt.Printf("改变slice2之前: slice1=%+v, slice2=%+v ", slice1, slice2)

        slice2[0] = 234

        fmt.Printf("改变slice2之后: slice1=%+v, slice2=%+v ", slice1, slice2)

    }

    ?

    1

    2

    3

    4

    5

    $ cd $GOPATH/src/basictype_test

    $ go build

    $ ./basictype_test

    改变slice2之前: slice1=[1 2 3 4 5], slice2=[3 4]

    改变slice2之后: slice1=[1 2 234 4 5], slice2=[234 4]

    slice1和slice2共用一个底层数组,修改slice2的元素导致slice1也发生变化。

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    package main

     

    import (

        "fmt"

    )

     

    func main() {

        var slice1 = []int32{1, 2, 3, 4, 5}

        var slice2 = slice1[2:4]

        fmt.Printf("改变slice2之前: slice1=%+v, slice2=%+v ", slice1, slice2)

        slice2 = append(slice2, 6, 7, 8)

        fmt.Printf("改变slice2之后: slice1=%+v, slice2=%+v ", slice1, slice2)

    }

    ?

    1

    2

    3

    4

    5

    $ cd $GOPATH/src/basictype_test

    $ go build

    $ ./basictype_test

    改变slice2之前: slice1=[1 2 3 4 5], slice2=[3 4]

    改变slice2之后: slice1=[1 2 3 4 5], slice2=[3 4 6 7 8]

    而append操作可令slice1或slice2重新分配底层数组,因此对slice1或slice2执行append操作都不会相互影响。

    接口类型

    接口在golang中的实现比较复杂,在$GOROOT/src/pkg/runtime/type.h中定义了:

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    struct Type

    {

        uintptr size;

        uint32 hash;

        uint8 _unused;

        uint8 align;

        uint8 fieldAlign;

        uint8 kind;

        Alg *alg;

        void *gc;

        String *string;

        UncommonType *x;

        Type *ptrto;

    };

    在$GOROOT/src/pkg/runtime/runtime.h中定义了:

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    struct Iface

    {

        Itab*   tab;

        void*   data;

    };

    struct Eface

    {

        Type*   type;

        void*   data;

    };

    struct  Itab

    {

        InterfaceType*  inter;

        Type*   type;

        Itab*   link;

        int32   bad;

        int32   unused;

        void    (*fun[])(void);

    };

    interface实际上是一个结构体,包括两个成员,一个是指向数据的指针,一个包含了成员的类型信息。Eface是interface{}底层使用的数据结构。因为interface中保存了类型信息,所以可以实现反射。反射其实就是查找底层数据结构的元数据。完整的实现在:$GOROOT/src/pkg/runtime/iface.c 。

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    package main

     

    import (

        "fmt"

        "unsafe"

    )

     

    func main() {

        var str interface{} = "Hello World!"

        p := (*struct {

            tab  uintptr

            data uintptr

        })(unsafe.Pointer(&str))

     

        fmt.Printf("%+v ", p)

    }

    ?

    1

    2

    3

    4

    $ cd $GOPATH/src/basictype_test

    $ go build

    $ ./basictype_test

    output: &{tab:134966528 data:406847688}

    map类型

    golang的map实现是hashtable,源码在:$GOROOT/src/pkg/runtime/hashmap.c 。

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    struct Hmap

    {

        uintgo  count;

        uint32  flags;

        uint32  hash0;

        uint8   B;

        uint8   keysize;

        uint8   valuesize;

        uint16  bucketsize;

     

        byte    *buckets;

        byte    *oldbuckets;

        uintptr nevacuate;

    };

    测试代码如下:

    ?

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    package main

     

    import (

        "fmt"

        "unsafe"

    )

     

    func main() {

        var m = make(map[string]int32, 10)

        m["hello"] = 123

        p := (*struct {

            count      int

            flags      uint32

            hash0      uint32

            B          uint8

            keysize    uint8

            valuesize  uint8

            bucketsize uint16

     

            buckets    uintptr

            oldbuckets uintptr

            nevacuate  uintptr

        })(unsafe.Pointer(&m))

     

        fmt.Printf("output: %+v ", p)

    }

    ?

    1

    2

    3

    4

    $ cd $GOPATH/src/basictype_test

    $ go build

    $ ./basictype_test

    output: &{count:407032064 flags:0 hash0:134958144 B:192 keysize:0 valuesize:64 bucketsize:30063 buckets:540701813 oldbuckets:0 nevacuate:0}

    golang的坑还是比较多的,需要深入研究底层,否则很容易掉坑里。

  • 相关阅读:
    关于js计算非等宽字体宽度的方法
    [NodeJs系列]聊一聊BOM
    Vue.js路由管理器 Vue Router
    vue 实践技巧合集
    微任务、宏任务与Event-Loop
    事件循环(EventLoop)的学习总结
    Cookie、Session和LocalStorage
    MySQL 树形结构 根据指定节点 获取其所在全路径节点序列
    MySQL 树形结构 根据指定节点 获取其所有父节点序列
    MySQL 创建函数报错 This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration and binary logging is enabled (you *might* want to use the less safe log_bin_trust_function_creators
  • 原文地址:https://www.cnblogs.com/moodlxs/p/4133121.html
Copyright © 2011-2022 走看看