這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
雖然golang是用C實現的,並且被稱為下一代的C語言,但是golang跟C的差別還是很大的。它定義了一套很豐富的資料類型及資料結構,這些類型和結構或者是直接映射為C的資料類型,或者是用C struct來實現。瞭解golang的資料類型和資料結構的底層實現,將有助於我們更好的理解golang並寫出品質更好的代碼。
基礎類型
源碼在:$GOROOT/src/pkg/runtime/runtime.h 。我們先來看下基礎類型:
/* * 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 _64BITtypedef uint64 uintptr;typedef int64 intptr;typedef int64 intgo; // Go's inttypedef uint64 uintgo; // Go's uint#elsetypedef uint32 uintptr;typedef int32 intptr;typedef int32 intgo; // Go's inttypedef uint32 uintgo; // Go's uint#endif/* * defined types */typedefuint8bool;typedefuint8byte;
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。可以看下測試:
package mainimport ( "fmt" "reflect")func main() { var b byte = 'D' fmt.Printf("output: %v\n", reflect.TypeOf(b).Kind())}
$ cd $GOPATH/src/basictype_test$ go build$ ./basictype_testoutput: uint8
資料類型分為靜態類型和底層類型,相對於以上代碼中的變數b來說,byte是它的靜態類型,uint8是它的底層類型。這點很重要,以後經常會用到這個概念。
rune類型
rune是int32的別名,用於表示unicode字元。通常在處理中文的時候需要用到它,當然也可以用range關鍵字。
string類型
string類型的底層是一個C struct。
struct String{ byte* str; intgo len;};
成員str為字元數組,len為字元數組長度。golang的字串是不可變類型,對string類型的變數初始化意味著會對底層結構的初始化。至於為什麼str用byte類型而不用rune類型,這是因為golang的for迴圈對字串的遍曆是基於位元組的,如果有必要,可以轉成rune切片或使用range來迭代。我們來看個例子:
$GOPATH/src
----basictype_test
--------main.go
package mainimport ("fmt""unsafe")func main() {var str string = "hi, 陳一回~"p := (*struct {str uintptrlen int})(unsafe.Pointer(&str))fmt.Printf("%+v\n", p)}
$ cd $GOPATH/src/basictype_test$ go build$ ./basictype_testoutput: &{str:135100456 len:14}
內建函數len對string類型的操作是直接從底層結構中取出len值,而不需要額外的操作,當然在初始化時必需同時初始化len的值。
slice類型
slice類型的底層同樣是一個C struct。
structSlice{// must not move anythingbyte*array;// actual datauintgolen;// number of elementsuintgocap;// allocated number of elements};
包括三個成員。array為底層數組,len為實際存放的個數,cap為總容量。使用內建函數make對slice進行初始化,也可以類似於數組的方式進行初始化。當使用make函數來對slice進行初始化時,第一個參數為切片類型,第二個參數為len,第三個參數可選,如果不傳入,則cap等於len。通常傳入cap參數來預先分配大小的slice,避免頻繁重新分配記憶體。
package mainimport ("fmt""unsafe")func main() {var slice []int32 = make([]int32, 5, 10)p := (*struct {array uintptrlen intcap int})(unsafe.Pointer(&slice))fmt.Printf("output: %+v\n", p)}
$ cd $GOPATH/src/basictype_test$ go build$ ./basictype_testoutput: &{array:406958176 len:5 cap:10}
由於切片指向一個底層數組,並且可以通過切片文法直接從數組產生切片,所以需要瞭解切片和數組的關係,否則可能就會不知不覺的寫出有bug的代碼。比如有如下代碼:
package mainimport ("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] = 234fmt.Printf("改變slice之後: array=%+v, slice=%+v\n", array, slice)}
$ 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表示總容量,添加或刪除操作不一定會使總容量發生變化。我們接著再來看另一個例子:
package mainimport ("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] = 234fmt.Printf("改變slice之後: array=%+v, slice=%+v\n", array, slice)}
$ 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。
但是很顯然,這種規則對從切片產生的切片也是同樣的,請看代碼:
package mainimport ("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] = 234fmt.Printf("改變slice2之後: slice1=%+v, slice2=%+v\n", slice1, slice2)}
$ 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也發生變化。
package mainimport ("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)}
$ 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中定義了:
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中定義了:
struct Iface{Itab*tab;void*data;};struct Eface{Type*type;void*data;};structItab{InterfaceType*inter;Type*type;Itab*link;int32bad;int32unused;void(*fun[])(void);};
interface實際上是一個結構體,包括兩個成員,一個是指向資料的指標,一個包含了成員的類型資訊。Eface是interface{}底層使用的資料結構。因為interface中儲存了類型資訊,所以可以實現反射。反射其實就是尋找底層資料結構的中繼資料。完整的實現在:$GOROOT/src/pkg/runtime/iface.c 。
package mainimport ("fmt""unsafe")func main() {var str interface{} = "Hello World!"p := (*struct {tab uintptrdata uintptr})(unsafe.Pointer(&str))fmt.Printf("%+v\n", p)}
$ cd $GOPATH/src/basictype_test$ go build$ ./basictype_testoutput: &{tab:134966528 data:406847688}
map類型
golang的map實現是hashtable,源碼在:$GOROOT/src/pkg/runtime/hashmap.c 。
struct Hmap{uintgo count;uint32 flags;uint32 hash0;uint8 B;uint8 keysize;uint8 valuesize;uint16 bucketsize;byte *buckets;byte *oldbuckets;uintptr nevacuate;};
測試代碼如下:
package mainimport ("fmt""unsafe")func main() {var m = make(map[string]int32, 10)m["hello"] = 123p := (*struct {count intflags uint32hash0 uint32B uint8keysize uint8valuesize uint8bucketsize uint16buckets uintptroldbuckets uintptrnevacuate uintptr})(unsafe.Pointer(&m))fmt.Printf("output: %+v\n", p)}
$ cd $GOPATH/src/basictype_test$ go build$ ./basictype_testoutput: &{count:407032064 flags:0 hash0:134958144 B:192 keysize:0 valuesize:64 bucketsize:30063 buckets:540701813 oldbuckets:0 nevacuate:0}
golang的坑還是比較多的,需要深入研究底層,否則很容易掉坑裡。