快捷搜索:  汽车  科技

go语言基本结构(go语言底层数据结构解析)

go语言基本结构(go语言底层数据结构解析)type = struct string {(gdb) ptype sgo build -gcflags "all=-N -l" -o test1 test1.gogdb test1下面是具体的一些指令

透过语言看原理,本文会借go语言,来说明下底层是现在基本数据结构的原理

字符串

首先我们需要知道c语言和go中对于字符串结构的不同,

go语言基本结构(go语言底层数据结构解析)(1)

c语言中是一块连续内存,以”\0”结尾,而go中则是两块地址,第一块是16字节,8字节的指针,8字节的长度,可以看下面的代码来验证:

go语言基本结构(go语言底层数据结构解析)(2)

接着我们用gdb调试代码,指令

go build -gcflags "all=-N -l" -o test1 test1.go

gdb test1

下面是具体的一些指令

(gdb) ptype s

type = struct string {

uint8 *str;

int len;

}

(gdb) x/2xg &s

0xc420047f68: 0x000000000106cae9 0x0000000000000007

# 此处 0x000000000106cae9 是字符串地址,0x0000000000000007 是长度

(gdb) x/7cb 0x000000000106cae9

0x106cae9 <go.string.* 601>: 97 'a' 98 'b' 99 'c' 100 'd' 101 'e' 102 'f' 103 'g'

(gdb) info files

0x0000000001001000 - 0x00000000010501f3 is .text

0x0000000001050200 - 0x00000000010740b2 is __TEXT.__rodata

# 可以看到字符串“abcdefg”是保存在__rodata中的

(gdb) p/x $rsp

$4 = 0xc420047f58

(gdb) p/x $rbp

$5 = 0xc420047f78

(gdb) p/x &s

$6 = 0xc420047f68

# 栈基址是0xc420047f78,向下增长到0xc420047f58,其中s是在栈中

(gdb) x/8xw 0xc420047f58

0xc420047f58: 0x2007a000 0x000000c4 0x0102dbc5 0x00000000

0xc420047f68: 0x0106cae9 0x00000000 0x00000007 0x00000000

# 可以看到确实是字符串s的内容

x的用法可以用help x查看

具体的演示视频可以看

[![asciicast](https://asciinema.org/a/195993.png)](https://asciinema.org/a/195993)

下面来回答一个问题,为什么所有语言把字符串实现为不可变类型?

  • 提高效率,通过相同字符串都引用同一副本,有效的减少了相同字符串的副本数量
  • 提高字符串比较的效率,通过计算hashcode,不同字符串的hashcode肯定不同,相同的hashcode,再比较字符串
  • 安全性,不管经过多少次传递,字符串的地址不会改变,防止了恶意的篡改

既然字符串都是不可变的,那对于字符串拼接的操作,我们需要怎么呢?

  1. 申请一块内存 buffer,将不可变字符串内容拷贝进来
  2. 然后修改这块内存 buffer 中的内容
  3. 再将这块 buffer 中的内容转化为不可变的字符串

可以看到对于一次字符串拼接,需要申请两次内存,那有什么优化方法呢?

下面来看下go中对于字符串拼接的处理。

func main() {

s1 := "aaa"

s2 := "bbb"

s3 := "ccc"

println(s1 s2 s3)

}

我们进行汇汇编后看具体的指令。

go语言基本结构(go语言底层数据结构解析)(3)

上面操作就是将字符串在栈上建立起来,然后3个字面量动态分配的地址是:

0x106cbe8,0x106cbeb,0x106cbee。

最后会调用到runtime.concatstring3函数:

func concatstring3(buf *tmpBuf a [3]string) string {

return concatstrings(buf a[:])

}

具体看汇编指令,以及相应的栈内存布局:

go语言基本结构(go语言底层数据结构解析)(4)

相应的栈布局:

go语言基本结构(go语言底层数据结构解析)(5)

可以看到栈上需要预留32字节的buf,以及3个字符串,其中返回值concatstrings是concatstrings函数的栈上内容。

通过上面的汇编指令,我们可以看到go中对于字符串拼接,是将其都复制到一块内存中,然后最后再将其转换为字符串返回,其中只有一次内存分配。

现在我们知道了go语言中对于字符串拼接的优化后,我们下面来看字符串与切片之间的转换,先看代码:

func toString(b []byte) string {

return string(b)

}

func unsafeToString(b []byte) string {

return *(*string)(unsafe.Pointer(&b))//b指针的数据结构

}

func main() {

b := []byte("hello world!")

s1 := toString(b)

s2 := unsafeToString(b)

println(s1 == s2)

println(s1 s2)

}

我们看汇编指令

go语言基本结构(go语言底层数据结构解析)(6)

可以看到是生成了一个string,然后再通过runtime.stringtoslicebyte将其转换为切片,看实现:

func stringtoslicebyte(buf *tmpBuf s string) []byte {

var b []byte

if buf != nil && len(s) <= len(buf) {

*buf = tmpBuf{} // 将buf内容清零

b = buf[:len(s)] // 此处将会赋值 b 的 cap,len,ptr字段

} else {

b = rawbyteslice(len(s))

}

copy(b s) # runtime.memmove,直接汇编实现 memmove_amd64.s

# func copy(dst src []Type) int

return b

}

通过上面我们以看到正常string如何转换为slice的,将string的ptr所指向内容拷贝到slice的ptr所指向的内存。

在内存布局上,slice和string的结构如下:

go语言基本结构(go语言底层数据结构解析)(7)

所以我们可以通过共用底层的byte array,达到高性能的转换。

总结下:上面我们看到了go在实现底层字符串上,通过将字符串不可变,能够极大的提高效率,同时我们如果想修改字符串,则需要通过重新申请内存的拷贝的方式来进行修改,同时我们可以看到,go语言对于32字节的内存,直接是在栈上进行内存分配的,这是对于短字符串的优化。另外,我们看到切片和字符串其结构布局是类似的,只是切片需要多一个cap来管理当前可用的最大内存,通过一些指针转换,我们可以极大的提高字符串和切片之间的转换效率。

数组

看完字符串和切片后,我们来看数组,首先也是c语言和go中数组的不同:

go语言基本结构(go语言底层数据结构解析)(8)

c语言中数组A,A代表了数组首地址,而go中数组A,A代表是整个数组元素,这就带来了在c语言中传递数组,传递的是指针,而go中传递数组,传递的是整个数组。

go语言基本结构(go语言底层数据结构解析)(9)

因此我们来看两种传递数组的不同:

func test1(x [3]int) { // 传递整个数组

println("test:" &x)

x[1] = 100

}

func test2(x *[3]int) { // 传递数组首地址

println("test:" x)

x[1] = 100

}

数组指针和指针数组

什么是指针?指针也是一个变量,只不过这个变量里存的是一个地址,所以数组指针是一个指针,里面存的是

数组地址,而指针数组是一个数组,数组中每个元素都是指针

如何对数组进行动态扩容?

当我们原先的数组容量不够的时候,我们需要重新申请一块内存,然后将数据拷贝过来。而为了使用这块新申请的内存,c语言可以使用指针类型转换,将新申请的内存使用起来,但是go语言默认是不支持指针的,所有需要有另一种结构来管理这块新申请的内存,这就是切片 切片的定义如下:

type slice struct {

array unsafe.Pointer

len int

cap int

}

其中array是指针,指向新申请的内存,len是这块内存的当前使用长度,cap是总的可使用长度。

go中new和make创建引用类型的区别

引用类型就是当前类型内部有一个指针引用另外一个数据结构,在go语言中有两种方式分配内存:new和make。

其中new是返回要分配类型的长度,具体说就是:new([8]byte)返回8byte,new([]byte)计算类型长度 (ptr(8bit) len(8bit) cap(8bit)),三个字段组成的24字节内存空间。

make则是一个语法糖,对于引用类型不仅申请类型本身,还会去申请所引用的内存,make([]byte 0 8)时候,首先会创建切片本身头对象 (ptr len cap),然后创建底层数组,数组容量是8个,然后把指针指向开始位置,len 设为0,cap 设为8。

上面我们知道了数组的结构,数组的扩容,已经数组的分配,数组在go中只支持静态数组,即数组大小必须是编译期就能够确定的,另外通过切片访问底层数组,会需要额外的一次寻址,这在关键地方会影响性能,因为额外的内存寻址可能意味着不能放入 L1-L3 cache,如果直接访问内存,这个速度就会慢很多。

最后附上一张常用硬件性能参数图。

go语言基本结构(go语言底层数据结构解析)(10)

猜您喜欢: