【GoLang】golang底层数据类型实现原理
虽然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的坑还是比较多的,需要深入研究底层,否则很容易掉坑里。
参考资料:
http://blog.csdn.net/sryan/article/details/46514883
【GoLang】golang底层数据类型实现原理的更多相关文章
- golang之基本数据类型
目录 一.golang之基本数据类型 1. 整型 (1)有符号(范围是负数.0和正数) (2)无符号(范围是0和正数) (3)特殊整型 (4)数字字面量语法 2. 浮点型 3. 复数类型 4. 布尔类 ...
- golang自己定义数据类型查询与插入postgresql中point数据
golang自己定义数据类型查询与插入postgresql中point数据 详细代码例如以下: package main import ( "bytes" "databa ...
- Golang的基础数据类型-字符串型
Golang的基础数据类型-字符串型 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.字符型概述 上一篇博客学习了使用单引号括起来的单个字符就是字符类型,在Golang中有两种表 ...
- Golang的基础数据类型-字符型
Golang的基础数据类型-字符型 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.字符型概述 Go语言中的字符有两种,即uint8类型和rune类型. uint8类型: 我们也 ...
- Golang的基础数据类型-浮点型
Golang的基础数据类型-浮点型 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.浮点型概述 Go语言提供两种精度的浮点数,即float32和float64,其中float32 ...
- Golang的基础数据类型-整型
Golang的基础数据类型-整型 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.整型概述 Go语言的整数类型一共有10个. int: 默认是有符号(signed)整形,占用空间 ...
- Golang的基础数据类型-布尔型
Golang的基础数据类型-布尔型 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.bool类型概述 bool类型的取值范围: bool类型的值只有两种,其值不为真即为假,可以用 ...
- Golang Sync.WaitGroup 使用及原理
Golang Sync.WaitGroup 使用及原理 使用 func main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.A ...
- 从底层谈WebGIS 原理设计与实现(三):WebGIS前端地图显示之根据地理范围换算出瓦片行列号的原理(转载)
从底层谈WebGIS 原理设计与实现(三):WebGIS前端地图显示之根据地理范围换算出瓦片行列号的原理 1.前言 在上一节中我们知道了屏幕上一像素等于实际中多少单位长度(米或经纬度)的换算方法, ...
随机推荐
- 使用Alcatraz来管理Xcode插件(转)
转自唐巧的博客:http://blog.devtang.com/blog/2014/03/05/use-alcatraz-to-manage-xcode-plugins/ 简介 Alcatraz是一个 ...
- jQ控制前端输入用户为空是的提醒
1.在jQ中$(function(){})函数的意思是 $(document).ready(function(){}) 的简写,用来在DOM加载完成之后执行一系列预先定义好的函数 placeholde ...
- Orchard源码分析(3):Orchard.WarmupStarter程序集
概述 Orchard.WarmupStarter程序集包含三个类:WarmupUtility.WarmupHttpModule和Starter<T>.该程序集主要为Orchard应用启动初 ...
- InstallShield 2010 使用 .net framework 4.5
一.InstallShield 2010 使用 .net framework 4.5记录 1.prq的地址,通过以下地址,下载相应的prq文件 .NET 4.5: http://saturn.inst ...
- linux的多媒体 播放 软件版权问题
linux下基本很多 跟多媒体 相关的软件, 都是有版权的, 都是 第三方软件, 都是closed-resource的 都有版权问题, 因此, 几乎所有的 linux的 发行版 都不会带有 多媒体软件 ...
- vim 使用技巧
2014-11-22 更新 文件abc 1,需要编辑abc的第三行 vim +3 abc 2,需要查询abc文件中的test字符 vim +/test abc 3,创建三个文件 aa bb cc vi ...
- RTX二次开发(二)(基于ASP.NET)
上一篇,我们讲到我开发环境的配置,还没配置好开发环境或再看一遍开发环境配置?接下来,我们开始coding...... 在coding之前,我们先添加引用. 我们在SDK的安装目录中引用这个文件. 引用 ...
- 密码学初级教程(三)公钥密码RSA
密码学家工具箱中的6个重要的工具: 对称密码 公钥密码 单向散列函数 消息认证码 数字签名 伪随机数生成器 公钥密码(非对称密码) 问题: 公钥认证问题 处理速度是对称密码的几百分之一 求离散对数非常 ...
- src 小心得
关于src的引用,不要用相对路径, ../ 虽然省事,但是跳转页面时容易出错. 举个例子: 在web页面引用 D:\phpStudy\WWW\ueditor\utf8-php 这个文件夹下面 ...
- Android 如何实现带滚动条的TextView,在更新文字时自动滚动到最后一行?
1.在布局文件中放置一个TextView,给它添加scrollbars和fadeScrollbars两个属性. 如下设置:滚动条为垂直滚动条,并且一直可见(当TextView中的文字行数超过页面能显示 ...