Go语言 常用数据类型底层结构分析
来源:互联网 发布:东航机长最新年薪 知乎 编辑:程序博客网 时间:2024/05/29 17:00
虽然golang是用C实现的,并且被称为下一代的C语言,但是golang跟C的差别还是很大的。它定义了一套很丰富的数据类型及数据结构,这些类型和结构或者是直接映射为C的数据类型,或者是用C struct来实现。了解golang的数据类型和数据结构的底层实现,将有助于我们更好的理解golang并写出质量更好的代码。
基础类型
源码在:$GOROOT/src/pkg/runtime/runtime.h 。我们先来看下基础类型:
01
/*
02
* basic types
03
*/
04
typedef
signed
char
int8;
05
typedef
unsigned
char
uint8;
06
typedef
signed
short
int16;
07
typedef
unsigned
short
uint16;
08
typedef
signed
int
int32;
09
typedef
unsigned
int
uint32;
10
typedef
signed
long
long
int
int64;
11
typedef
unsigned
long
long
int
uint64;
12
typedef
float
float32;
13
typedef
double
float64;
14
15
#ifdef _64BIT
16
typedef
uint64 uintptr;
17
typedef
int64 intptr;
18
typedef
int64 intgo;
// Go's int
19
typedef
uint64 uintgo;
// Go's uint
20
#else
21
typedef
uint32 uintptr;
22
typedef
int32 intptr;
23
typedef
int32 intgo;
// Go's int
24
typedef
uint32 uintgo;
// Go's uint
25
#endif
26
27
/*
28
* defined types
29
*/
30
typedef
uint8
bool
;
31
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。可以看下测试:
01
package main
02
03
import (
04
"fmt"
05
"reflect"
06
)
07
08
func main() {
09
var b byte =
'D'
10
fmt.Printf(
"output: %v\n"
, reflect.TypeOf(b).Kind())
11
}
1
$
cd
$GOPATH/src/basictype_test
2
$ go build
3
$ ./basictype_test
4
output: uint8
数据类型分为静态类型和底层类型,相对于以上代码中的变量b来说,byte是它的静态类型,uint8是它的底层类型。这点很重要,以后经常会用到这个概念。
rune类型
rune是int32的别名,用于表示unicode字符。通常在处理中文的时候需要用到它,当然也可以用range关键字。
string类型
string类型的底层是一个C struct。
1
struct
String
2
{
3
byte* str;
4
intgo len;
5
};
成员str为字符数组,len为字符数组长度。golang的字符串是不可变类型,对string类型的变量初始化意味着会对底层结构的初始化。至于为什么str用byte类型而不用rune类型,这是因为golang的for循环对字符串的遍历是基于字节的,如果有必要,可以转成rune切片或使用range来迭代。我们来看个例子:
$GOPATH/src
----basictype_test
--------main.go
01
package main
02
03
import (
04
"fmt"
05
"unsafe"
06
)
07
08
func main() {
09
var str string =
"hi, 陈一回~"
10
p := (*
struct
{
11
str uintptr
12
len
int
13
})(unsafe.Pointer(&str))
14
15
fmt.Printf(
"%+v\n"
, p)
16
}
1
$
cd
$GOPATH/src/basictype_test
2
$ go build
3
$ ./basictype_test
4
output: &{str:135100456 len:14}
内建函数len对string类型的操作是直接从底层结构中取出len值,而不需要额外的操作,当然在初始化时必需同时初始化len的值。
slice类型
slice类型的底层同样是一个C struct。
1
struct
Slice
2
{
// must not move anything
3
byte* array;
// actual data
4
uintgo len;
// number of elements
5
uintgo cap;
// allocated number of elements
6
};
包括三个成员。array为底层数组,len为实际存放的个数,cap为总容量。使用内建函数make对slice进行初始化,也可以类似于数组的方式进行初始化。当使用make函数来对slice进行初始化时,第一个参数为切片类型,第二个参数为len,第三个参数可选,如果不传入,则cap等于len。通常传入cap参数来预先分配大小的slice,避免频繁重新分配内存。
01
package main
02
03
import (
04
"fmt"
05
"unsafe"
06
)
07
08
func main() {
09
var slice []int32 = make([]int32, 5, 10)
10
p := (*
struct
{
11
array uintptr
12
len
int
13
cap
int
14
})(unsafe.Pointer(&slice))
15
16
fmt.Printf(
"output: %+v\n"
, p)
17
}
1
$
cd
$GOPATH/src/basictype_test
2
$ go build
3
$ ./basictype_test
4
output: &{array:406958176 len:5 cap:10}
由于切片指向一个底层数组,并且可以通过切片语法直接从数组生成切片,所以需要了解切片和数组的关系,否则可能就会不知不觉的写出有bug的代码。比如有如下代码:
01
package main
02
03
import (
04
"fmt"
05
)
06
07
func main() {
08
var array = [...]int32{1, 2, 3, 4, 5}
09
var slice = array[2:4]
10
fmt.Printf(
"改变slice之前: array=%+v, slice=%+v\n"
, array, slice)
11
slice[0] = 234
12
fmt.Printf(
"改变slice之后: array=%+v, slice=%+v\n"
, array, slice)
13
}
1
$ cd $GOPATH/src/basictype_test
2
$ go build
3
$ ./basictype_test
4
改变slice之前: array=[1 2 3 4 5], slice=[3 4]
5
改变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表示总容量,添加或删除操作不一定会使总容量发生变化。我们接着再来看另一个例子:
01
package main
02
03
import (
04
"fmt"
05
)
06
07
func main() {
08
var array = [...]int32{1, 2, 3, 4, 5}
09
var slice = array[2:4]
10
slice = append(slice, 6, 7, 8)
11
fmt.Printf(
"改变slice之前: array=%+v, slice=%+v\n"
, array, slice)
12
slice[0] = 234
13
fmt.Printf(
"改变slice之后: array=%+v, slice=%+v\n"
, array, slice)
14
}
1
$
cd
$GOPATH/src/basictype_test
2
$ go build
3
$ ./basictype_test
4
改变slice之前: array=[1 2 3 4 5], slice=[3 4 6 7 8]
5
改变slice之后: array=[1 2 3 4 5], slice=[234 4 6 7 8]
经过append操作之后,对slice的修改并未影响到array。原因在于append的操作令slice重新分配底层数组,所以此时slice的底层数组不再指向前面定义的array。
但是很显然,这种规则对从切片生成的切片也是同样的,请看代码:
01
package main
02
03
import (
04
"fmt"
05
)
06
07
func main() {
08
var slice1 = []int32{1, 2, 3, 4, 5}
09
var slice2 = slice1[2:4]
10
fmt.Printf(
"改变slice2之前: slice1=%+v, slice2=%+v\n"
, slice1, slice2)
11
slice2[0] = 234
12
fmt.Printf(
"改变slice2之后: slice1=%+v, slice2=%+v\n"
, slice1, slice2)
13
}
1
$
cd
$GOPATH/src/basictype_test
2
$ go build
3
$ ./basictype_test
4
改变slice2之前: slice1=[1 2 3 4 5], slice2=[3 4]
5
改变slice2之后: slice1=[1 2 234 4 5], slice2=[234 4]
slice1和slice2共用一个底层数组,修改slice2的元素导致slice1也发生变化。
01
package main
02
03
import (
04
"fmt"
05
)
06
07
func main() {
08
var slice1 = []int32{1, 2, 3, 4, 5}
09
var slice2 = slice1[2:4]
10
fmt.Printf(
"改变slice2之前: slice1=%+v, slice2=%+v\n"
, slice1, slice2)
11
slice2 = append(slice2, 6, 7, 8)
12
fmt.Printf(
"改变slice2之后: slice1=%+v, slice2=%+v\n"
, slice1, slice2)
13
}
1
$
cd
$GOPATH/src/basictype_test
2
$ go build
3
$ ./basictype_test
4
改变slice2之前: slice1=[1 2 3 4 5], slice2=[3 4]
5
改变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中定义了:
01
struct
Type
02
{
03
uintptr size;
04
uint32 hash;
05
uint8 _unused;
06
uint8 align;
07
uint8 fieldAlign;
08
uint8 kind;
09
Alg *alg;
10
void
*gc;
11
String *string;
12
UncommonType *x;
13
Type *ptrto;
14
};
在$GOROOT/src/pkg/runtime/runtime.h中定义了:
01
struct
Iface
02
{
03
Itab* tab;
04
void
* data;
05
};
06
struct
Eface
07
{
08
Type* type;
09
void
* data;
10
};
11
struct
Itab
12
{
13
InterfaceType* inter;
14
Type* type;
15
Itab* link;
16
int32 bad;
17
int32 unused;
18
void
(*fun[])(
void
);
19
};
interface实际上是一个结构体,包括两个成员,一个是指向数据的指针,一个包含了成员的类型信息。Eface是interface{}底层使用的数据结构。因为interface中保存了类型信息,所以可以实现反射。反射其实就是查找底层数据结构的元数据。完整的实现在:$GOROOT/src/pkg/runtime/iface.c 。
01
package main
02
03
import (
04
"fmt"
05
"unsafe"
06
)
07
08
func main() {
09
var str interface{} =
"Hello World!"
10
p := (*
struct
{
11
tab uintptr
12
data uintptr
13
})(unsafe.Pointer(&str))
14
15
fmt.Printf(
"%+v\n"
, p)
16
}
1
$
cd
$GOPATH/src/basictype_test
2
$ go build
3
$ ./basictype_test
4
output: &{tab:134966528 data:406847688}
map类型
golang的map实现是hashtable,源码在:$GOROOT/src/pkg/runtime/hashmap.c 。
01
struct
Hmap
02
{
03
uintgo count;
04
uint32 flags;
05
uint32 hash0;
06
uint8 B;
07
uint8 keysize;
08
uint8 valuesize;
09
uint16 bucketsize;
10
11
byte *buckets;
12
byte *oldbuckets;
13
uintptr nevacuate;
14
};
测试代码如下:
01
package main
02
03
import (
04
"fmt"
05
"unsafe"
06
)
07
08
func main() {
09
var m = make(map[string]int32, 10)
10
m[
"hello"
] = 123
11
p := (*
struct
{
12
count
int
13
flags uint32
14
hash0 uint32
15
B uint8
16
keysize uint8
17
valuesize uint8
18
bucketsize uint16
19
20
buckets uintptr
21
oldbuckets uintptr
22
nevacuate uintptr
23
})(unsafe.Pointer(&m))
24
25
fmt.Printf(
"output: %+v\n"
, p)
26
}
1
$
cd
$GOPATH/src/basictype_test
2
$ go build
3
$ ./basictype_test
4
output: &{count:407032064 flags:0 hash0:134958144 B:192 keysize:0 valuesize:64 bucketsize:30063 buckets:540701813 oldbuckets:0 nevacuate:0}
golang的坑还是比较多的,需要深入研究底层,否则很容易掉坑里。
- Go语言 常用数据类型底层结构分析
- Go语言 常用数据类型底层结构分析
- Go语言 常用数据类型底层结构分析
- golang: 常用数据类型底层结构分析
- golang: 常用数据类型底层结构分析
- [go语言]基本数据类型
- Go 语言数据类型
- go语言基本数据类型
- 4.Go 语言数据类型
- 3.Go语言数据类型
- GO语言常见数据类型
- Go语言结构示例
- Go 语言结构
- Go 语言结构体
- Go 语言结构体
- go语言结构体
- 2.GO 语言结构
- Go语言-结构体
- jsp9大内置对象详解
- xcode创建一个游戏项目
- C#-文件读取数据至dataset以更新database
- 【笔记】cisco路由器动态IP接入NAT设定
- 设计模式 --3.单一职责原则,4.开发-封闭原则,5依赖倒转原则
- Go语言 常用数据类型底层结构分析
- String,StringBuffer,StringBuilder个人认为较重要的区别
- 【C语言】题目1001:A+B for Matrices【学好英语很重要】
- cookie和session(宇宙级框架express)
- PS快捷键大全
- C++实现栈应用之进制转换
- 数据库连接JDBC
- linux shell history 的经典用法
- rocketmq的优雅停机