golang: 常用數據類型底層結構分析

轉自 http://www.cnblogs.com/moodlxs/p/4133121.html


雖然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\n", 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\n", 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\n", 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\n", array, slice)

    slice[0] = 234

    fmt.Printf("改變slice之後: array=%+v, slice=%+v\n", 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\n", array, slice)

    slice[0] = 234

    fmt.Printf("改變slice之後: array=%+v, slice=%+v\n", 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\n", slice1, slice2)

    slice2[0] = 234

    fmt.Printf("改變slice2之後: slice1=%+v, slice2=%+v\n", 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\n", slice1, slice2)

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

    fmt.Printf("改變slice2之後: slice1=%+v, slice2=%+v\n", 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\n", 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\n", 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的坑還是比較多的,需要深入研究底層,否則很容易掉坑裏。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章