虽然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 _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。
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类型
- rune是int32的别名,用于表示unicode字符。通常在处理中文的时候需要用到它,当然也可以用range关键字。
string类型
源码位置:$GOROOT/src/string.go
string类型的底层是一个C struct。
struct String
{
byte* str;
intgo len;
};
type stringStruct struct {
str unsafe.Pointer
len int
}
- 成员str为字符数组,len为字符数组长度。
- golang的字符串是不可变类型,对string类型的变量初始化意味着会对底层结构的初始化。
- 至于为什么str用byte类型而不用rune类型,这是因为golang的for循环对字符串的遍历是基于字节的,如果有必要,可以转成rune切片或使用range来迭代。
$GOPATH/src/basictype_test/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类型
源码位置:$GOOT/src/slice.go
slice类型的底层同样是一个C struct。
struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
type slice struct {
array unsafe.Pointer
len int
cap int
}
- 包括三个成员。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/runtime/runtime2.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中保存了类型信息,所以可以实现反射。反射其实就是查找底层数据结构的元数据。
package main
import (
"fmt"
"unsafe"
)
func main() {
var str interface{} = "Hello World!"
p := (*struct {
tab uintptr
data uintptr
})(unsafe.Pointer(&str))
fmt.Println("interface{}.data大小:", unsafe.Sizeof(p.data))
fmt.Println("interface{}.tab:", unsafe.Sizeof(p.tab))
fmt.Printf("%+v\n", p)
}
$ cd $GOPATH/src/basictype_test
$ go build
$ ./basictype_test
output:
interface{}.data大小: 8
interface{}.tab: 8
&{tab:134966528 data:406847688}
map类型
golang的map实现是hashtable,源码在:$GOROOT/src/runtime/hashmap.go 。
type hmap struct {
// Note: the format of the Hmap is encoded in ../../cmd/internal/gc/reflect.go and
// ../reflect/type.go. Don't change this structure without also changing that code!
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
测试代码如下:
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}