虽然golang是用C实现的,而且被称为下一代的C语言,可是golang跟C的差异仍是很大的。它定义了一套很丰富的数据类型及数据结构,这些类型和结构或者是直接映射为C的数据类型,或者是用C struct来实现。了解golang的数据类型和数据结构的底层实现,将有助于咱们更好的理解golang并写出质量更好的代码。golang
源码在:$GOROOT/src/pkg/runtime/runtime.h 。咱们先来看下基础类型:shell
/* * 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;
int八、uint八、int1六、uint1六、int3二、uint3二、int6四、uint6四、float3二、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个字节。因此若是有明确的要求,应该选择int3二、int64或uint3二、uint64。byte类型的底层类型是uint8。能够看下测试:数组
package main import ( "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_test output: uint8
数据类型分为静态类型和底层类型,相对于以上代码中的变量b来讲,byte是它的静态类型,uint8是它的底层类型。这点很重要,之后常常会用到这个概念。数据结构
rune是int32的别名,用于表示unicode字符。一般在处理中文的时候须要用到它,固然也能够用range关键字。
app
string类型的底层是一个C struct。函数
struct String { byte* str; intgo len; };
成员str为字符数组,len为字符数组长度。golang的字符串是不可变类型,对string类型的变量初始化意味着会对底层结构的初始化。至于为何str用byte类型而不用rune类型,这是由于golang的for循环对字符串的遍历是基于字节的,若是有必要,能够转成rune切片或使用range来迭代。咱们来看个例子:测试
$GOPATH/srcui
----basictype_testspa
--------main.go指针
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) }
$ cd $GOPATH/src/basictype_test $ go build $ ./basictype_test output: &{str:135100456 len:14}
内建函数len对string类型的操做是直接从底层结构中取出len值,而不须要额外的操做,固然在初始化时必需同时初始化len的值。
slice类型的底层一样是一个C struct。
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,避免频繁从新分配内存。
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) }
$ cd $GOPATH/src/basictype_test $ go build $ ./basictype_test output: &{array:406958176 len:5 cap:10}
因为切片指向一个底层数组,而且能够经过切片语法直接从数组生成切片,因此须要了解切片和数组的关系,不然可能就会不知不觉的写出有bug的代码。好比有以下代码:
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) }
$ 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 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) }
$ 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 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) }
$ 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 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) }
$ 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; }; 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 。
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) }
$ cd $GOPATH/src/basictype_test $ go build $ ./basictype_test output: &{tab:134966528 data:406847688}
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 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) }
$ 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的坑仍是比较多的,须要深刻研究底层,不然很容易掉坑里。