虽然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的坑还是比较多的,需要深入研究底层,否则很容易掉坑里。

golang: 常用数据类型底层结构分析的更多相关文章

  1. Redis常用数据类型底层数据结构分析

    Redis是一种键值(key-Value)数据库,相对于关系型数据库,它也被叫作非关系型数据库 Redis中,键的数据类型是字符串,但是为了非富数据存储方式,方便开发者使用,值的数据类型有很多 字符串 ...

  2. GoLang基础数据类型--->字典(map)详解

    GoLang基础数据类型--->字典(map)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.   可能大家刚刚接触Golang的小伙伴都会跟我一样,这个map是干嘛的,是 ...

  3. golang ----rune数据类型

    查询,官方的解释如下: // rune is an alias for int32 and is equivalent to int32 in all ways. It is // used, by ...

  4. Redis简介、与memcached比较、存储方式、应用场景、生产经验教训、安全设置、key的建议、安装和常用数据类型介绍、ServiceStack.Redis使用(1)

    1.NOSQL简介 nosql的产生并不是要彻底的代替关系型数据库,而是作为传统关系型数据库的一个补充. Facebook和360使用Cassandra来存储海量社交数据 Twitter在其url抓取 ...

  5. GoLang基础数据类型-切片(slice)详解

    GoLang基础数据类型-切片(slice)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 数组的长度在定义之后无法再次修改:数组是值类型,每次传递都将产生一份副本.显然这种数 ...

  6. GoLang基础数据类型--->数组(array)详解

    GoLang基础数据类型--->数组(array)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Golang数组简介 数组是Go语言编程中最常用的数据结构之一.顾名 ...

  7. c++ 常用数据类型,命名规则, 不常有数据类型

    1. 常用数据类型 最大值0111111111111111 = 32767最小值1000000000000000 = -32768 short 最低16位 2**7 - 1 负值:反码 int 至少和 ...

  8. 《闲扯Redis五》List数据类型底层之quicklist

    一.前言 Redis 提供了5种数据类型:String(字符串).Hash(哈希).List(列表).Set(集合).Zset(有序集合),理解每种数据类型的特点对于redis的开发和运维非常重要. ...

  9. SQLAlchemy02 /SQLAlchemy对数据的增删改查操作、属性常用数据类型详解

    SQLAlchemy02 /SQLAlchemy对数据的增删改查操作.属性常用数据类型详解 目录 SQLAlchemy02 /SQLAlchemy对数据的增删改查操作.属性常用数据类型详解 1.用se ...

随机推荐

  1. 51. N-Queens

    题目: The n-queens puzzle is the problem of placing n queens on an n×n chessboard such that no two que ...

  2. 面向切面编程AOP

    本文的主要内容(AOP): 1.AOP面向切面编程的相关概念(思想.原理.相关术语) 2.AOP编程底层实现机制(动态代理机制:JDK代理.Cglib代理) 3.Spring的传统AOP编程的案例(计 ...

  3. Git版本控制管理学习笔记1-介绍

    几乎所有的版本控制工具都是出于同样的目的:开发以及维护开发出来的代码,方便读取代码的历史,记录所有的修改.这里,介绍的是当前在开源社区内非常流行的版本控制工具Git.它是由Linus Torvalds ...

  4. AgileEAS.NET SOA 中间件平台5.2版本下载、配置学习(四):开源的Silverlight运行容器的编译、配置

    一.前言 AgileEAS.NET SOA 中间件平台是一款基于基于敏捷并行开发思想和Microsoft .Net构件(组件)开发技术而构建的一个快速开发应用平台.用于帮助中小型软件企业建立一条适合市 ...

  5. IBM x3850 x5 服务器 安装 Windows Server 2008

    一.硬件需求 一个8G以上的U盘 二.软件需求 1.Windwos Server 2008镜像 2.系统启动盘制作工具Ultraiso 3.IBM ServerGuide引导镜像 三.制作及安装步骤 ...

  6. Could not load file or assembly 'System.ServiceModel.DomainServices.Hosting'.系统找不到指定文件

    项目部署到服务器后出现如下错误信息: Parser Error Message: Could not load file or assembly 'System.ServiceModel.Domain ...

  7. JavaScript中两个感叹号的作用

    先用一个简单的例子说明:var o={flag:true};var test=!!o.flag;//等效于var test=o.flag||false;alert(test);由于对null与unde ...

  8. Vue#表单控件绑定

    使用v-model 在表单控件上实现数据双向绑定. 单选:https://jsfiddle.net/miloer/bs49p0fx/ <input type="checkbox&quo ...

  9. C#delegate委托

    类似函数,却没有语句体. using System; using System.Collections.Generic; using System.Linq; using System.Text; u ...

  10. jquery.validate.js表单验证

    一.用前必备官方网站:http://bassistance.de/jquery-plugins/jquery-plugin-validation/ API: http://jquery.bassist ...