zoukankan      html  css  js  c++  java
  • 【GoLang】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的坑还是比较多的,需要深入研究底层,否则很容易掉坑里。

    参考资料:

    http://blog.csdn.net/sryan/article/details/46514883

  • 相关阅读:
    蓝奏云的链接打不开
    数据链路层
    markdown(md)
    物理层
    计算机网络概述
    86五笔学习笔记
    Ubuntu初次使用的问题
    Hello Word!
    快速创建Flask Restful API项目
    Ubuntu环境下部署Django+uwsgi+nginx总结
  • 原文地址:https://www.cnblogs.com/junneyang/p/6203710.html
Copyright © 2011-2022 走看看