golang: 常用数据类型底层结构分析-程序员宅基地

技术标签: golang  runtime  数据结构与算法  

虽然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类型

string类型的底层是一个C struct。

struct String
{
        byte*   str;
        intgo   len;
};

成员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类型

slice类型的底层同样是一个C struct。

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,避免频繁重新分配内存。

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/pkg/runtime/type.h中定义了:

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中定义了:

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 。

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)
}

$ cd $GOPATH/src/basictype_test
$ go build
$ ./basictype_test
output: &{tab:134966528 data:406847688}

map类型

golang的map实现是hashtable,源码在:$GOROOT/src/pkg/runtime/hashmap.c 。

struct Hmap
{
	uintgo  count;
	uint32  flags;
	uint32  hash0;
	uint8   B;
	uint8   keysize;
	uint8   valuesize;
	uint16  bucketsize;

	byte    *buckets;
	byte    *oldbuckets;
	uintptr nevacuate;
};

测试代码如下:

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}

golang的坑还是比较多的,需要深入研究底层,否则很容易掉坑里。

转载于:https://my.oschina.net/goal/blog/196891

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_34033624/article/details/91515298

智能推荐

使用UmcFramework和unimrcpclient.xml连接多个SIP设置的配置指南及C代码示例

在多媒体通信领域,MRCP(Media Resource Control Protocol)协议被广泛用于控制语音识别和合成等媒体资源。UniMRCP是一个开源的MRCP实现,提供了客户端和服务端的库。UmcFramework是一个基于UniMRCP客户端库的示例应用程序框架,它帮助开发者快速集成和测试MRCP客户端功能。本文将详细介绍如何使用UmcFramework和unimrcpclient.xml配置文件连接到多个SIP设置,以及如何用C代码进行示例说明。

java.net.ProtocolException: Server redirected too many times (20)-程序员宅基地

文章浏览阅读3k次。报错:java.net.ProtocolException: Server redirected too many times (20)1.没有检查到cookie,一直循环重定向。解决:CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));URL url = new URL(url); ..._java.net.protocolexception: server redirected too many times (20)

springboot启动报错 Failed to scan *****/derbyLocale_ja_JP.jar from classloader hierarchy_failed to scan from classloader hierarchy-程序员宅基地

文章浏览阅读4.1k次。问题这是部分报错信息2019-07-11 14:03:34.283 WARN [restartedMain][DirectJDKLog.java:175] - Failed to scan [file:/D:/repo/org/apache/derby/derby/10.14.2.0/derbyLocale_ja_JP.jar] from classloader hierarchyjava...._failed to scan from classloader hierarchy

MATLAB-ones函数_matlab中ones函数-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏7次。在MATLAB中,ones函数用于创建一个指定大小的由1组成的矩阵或数组。_matlab中ones函数

解决PS等软件出现应用程序无法正常启动(0xc000007b)_photoshop应用程序无法正常启动0xc000007b。请单击“确认”关闭应用程序。-程序员宅基地

文章浏览阅读3.9w次,点赞2次,收藏9次。  在使用电脑办公过程中,安装应用程序时难免遇到无法安装或者无法正常启动的问题,这对我们使用电脑带来了诸多不便。那遇到应用程序无法正常启动的问题要如何解决呢?相信大家肯定都是十分疑问的,每次都是只能忍痛重新安装软件。今天,小编就和大家探讨下应用程序无法正常启动的解决方法,帮助大家排忧解难。0xc000007b电脑图解1  第一种方案:SFC检查系统完整性来尝试修复丢失文件  1、打开电脑搜索输入cmd.exe,选择以管理员身份运行,跳出提示框时选择继续。0xc000007b电脑图解2_photoshop应用程序无法正常启动0xc000007b。请单击“确认”关闭应用程序。

oracle介质恢复和实例恢复的异同-程序员宅基地

文章浏览阅读396次。1、概念 REDO LOG是Oracle为确保已经提交的事务不会丢失而建立的一个机制。实际上REDO LOG的存在是为两种场景准备的:实例恢复(INSTANCE RECOVERY);介质恢复(MEDIA RECOVERY)。 实例恢复的目的是在数据库发生故障时,确保BUFFER CACHE中的数据不会丢失,不会造成数据库的..._oracle 实例恢复和介质恢复

随便推点

轻松搭建CAS 5.x系列(5)-增加密码找回和密码修改功能-程序员宅基地

文章浏览阅读418次。概述说明CAS内置了密码找回和密码修改的功能; 密码找回功能是,系统会吧密码重置的连接通过邮件或短信方式发送给用户,用户点击链接后就可以重置密码,cas还支持预留密码重置的问题,只有回答对了,才可以重置密码;系统可配置密码重置后,是否自动登录; 密码修改功能是,用户登录后输入新密码即可完成密码修改。安装步骤`1. 首先,搭建好cas sso server您需要按..._修改cas默认用户密码

springcloud(七) feign + Hystrix 整合 、-程序员宅基地

文章浏览阅读141次。之前几章演示的熔断,降级 都是 RestTemplate + Ribbon 和RestTemplate + Hystrix ,但是在实际开发并不是这样,实际开发中都是 Feign 远程接口调用。Feign + Hystrix 演示:  eruka(略)order 服务工程:  pom.xml<?xml version="1.0" encoding="U..._this is order 服务工程

YOLOv7如何提高目标检测的速度和精度,基于优化算法提高目标检测速度-程序员宅基地

文章浏览阅读3.4k次,点赞35次,收藏43次。学习率是影响目标检测精度和速度的重要因素之一。合适的学习率调度策略可以加速模型的收敛和提高模型的精度。在YOLOv7算法中,可以使用基于余弦函数的学习率调度策略(Cosine Annealing Learning Rate Schedule)来调整学习率。

linux中进程退出函数:exit()和_exit()的区别_linux结束进程可以用哪些函数,它们之间有何区别?-程序员宅基地

文章浏览阅读4k次,点赞4次,收藏9次。 linux中进程退出函数:exit()和_exit()的区别(1)_exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。(2)调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin, stdout, stderr ...). exit函数是在_exit..._linux结束进程可以用哪些函数,它们之间有何区别?

sqlserver55555_sqlserver把小数点后面多余的0去掉-程序员宅基地

文章浏览阅读134次。select 5000/10000.0 --想变成0.5select 5500/10000.0 --想变成0.55select 5550/10000.0 --想变成0.555select 5555/10000.0 --想变成0.5555其结果分别为:0.5000000 0.5500000 0.5550000 0.5555000一、如果想去掉数字5后面多余的0 ,需要转化一下:selec..._sql server 去小数 0

Angular6 和 RXJS6 的一些改动_angular6,requestoptions改成了什么-程序员宅基地

文章浏览阅读3.1k次。例一:import { Injectable } from '@angular/core';import { Observable } from 'rxjs';import { User } from "./model/User";import { map } from 'rxjs/operators';import { Http, Response, Headers, RequestOp..._angular6,requestoptions改成了什么

推荐文章

热门文章

相关标签