虽然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. 如何从零基础学习VR

    转载请声明转载地址:http://www.cnblogs.com/Rodolfo/,违者必究. 近期很多搞技术的朋友问我,如何步入VR的圈子?如何从零基础系统性的学习VR技术? 本人将于2017年1月 ...

  2. UE移植到SAE云平台

    应用架在新浪的SAE上,而同时功能中又需要用上编辑器,鉴于百度的UEditor功能强大,可定制,文档全,所以理所当然的用它.而新浪把本地文件的IO操作禁止了,使得UEdiotr的图片上传.附件和在线涂 ...

  3. C++中常见的两种二义性问题及其解决方式

    --------------------------------一.“倒三角”二义性问题------------------------------- 问题描述:卤煮之所以称之为“倒三角问题”,是因为 ...

  4. [BI项目记]-搭建代码管理环境之签入代码

    此篇主要介绍如何基于TFS环境创建进行BI项目最简单的代码签入和签出工作. 首先介绍BI项目中的SSAS项目.SSAS项目主要包含Cube多维数据集和数据挖掘模型,是在微软BI产品体系中重要的一个组件 ...

  5. Knockout.js随手记(1)

    新的开始,knockout.js 1.首先去http://knockoutjs.com/index.html下载knockout.js,最新的版本是2.3 2.知道什么是Knockout?它是个Jav ...

  6. NPOIExcel

    public class NPOIExcel { private string _title; private string _sheetName; private string _filePath; ...

  7. tomcat配置详解/优化方案

    Service.xml Server.xml配置文件用于对整个容器进行相关的配置. <Server>元素:是整个配置文件的根元素.表示整个Catalina容器. 属性:className: ...

  8. C#连接Access数据库(详解)

    做一个用VS2012的C#连接Access数据库的备忘, SQL数据库固然强大,有大微软的强力技术支持,LINQ的方便操作,但是如果写一个小程序对数据库方面没有什么大的要求的话,将来在数据库方面就可以 ...

  9. 1.0 UIApplication对象

    本文并非最终版本,如有更新或更正会第一时间置顶,联系方式详见文末 如果觉得本文内容过长,请前往本人 “简书”   UIApplication对象特点: 特点1: UIApplication对象是应用程 ...

  10. vim 编辑