Go源码学习: string的内部数据结构

Go源码学习: string的内部数据结构

2021-11-15
Go

前面在学习Go程序进程的内存布局时,分析了一个Go程序在Linux下ELF文件格式经过简化后大致如下图所示:

go-elf-fmt.png

Go二进制文件ELF主要有这几个section组成: .text, .rodata, .data, .noptrdata, .bss, .noptrbss.rodata中存放的是常量数据,程序中的字面量在编译时会被放到这个section中,那么字符串字面量也将放到这个section中。 在执行程序时,ELF文件被加载到内存中,相同权限的Section会被对应到一个Segment中,大致是下图的内存加载结构:

elf-mem-load.png

程序在运行时字符串字面量在内存结构中的代码区。本文将学习Go语言中string的内部数据结构。

string的内部数据结构 #

string在Go中的内部结构是reflect.StringHeader位于reflect/value.go

 1// StringHeader is the runtime representation of a string.
 2// It cannot be used safely or portably and its representation may
 3// change in a later release.
 4// Moreover, the Data field is not sufficient to guarantee the data
 5// it references will not be garbage collected, so programs must keep
 6// a separate, correctly typed pointer to the underlying data.
 7type StringHeader struct {
 8	Data uintptr
 9	Len  int
10}

从StringHeader的注释中可以看出: StringHeader是string在运行时的表示形式,它本身不存储string的数据,而只包含一个指向string底层存储的指针(Data uinptr)和一个表示string长度的int字段(Len int),string的底层存储是一个byte类型的数组。

go-string-header.png

关于uintptr Go语言规范https://golang.org/ref/spec中是这样描述的an unsigned integer large enough to store the uninterpreted bits of a pointer value,是一个无符号整形大到可以存储任意一个指针的值。对比uint是和平台相关的无符号整形,在32位机器上是32位无符号整形,在64位机器上是64位无符号整形。

在Go中unsafe.Pointer表示任意类型的指针,可以将一个string的地址显式转换成unsafe.Pointer,就可以进一步显式转换成*reflect.StringHeader了:

 1package main
 2import (
 3	"fmt"
 4	"reflect"
 5	"unsafe"
 6)
 7func main() {
 8	s := "hello"
 9	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
10	fmt.Printf("%+v\n", sh) // &{Data:17446332 Len:5}
11	fmt.Println(sh.Len)     // 5
12	ptr := unsafe.Pointer(sh.Data)
13	arrPtr := (*[5]byte)(ptr)
14	fmt.Println(*arrPtr) // [104 101 108 108 111]
15}

上面的代码在第9行得到sh这个reflect.StringHeader类型的指针,后边可以获取它的底层数据数组指针,并继续通过通用指针unsafe.Pointer的转换能力得到底层字节数组。

有了上面对string内部数据结构了解,就不难立即当使用refect.Sizeof函数获取string类型占用的字节数时,在64位机器上,无论字符长度是多少,返回值都是16,即16个字节,这是因为refect.Sizeof获取string类型占用字节数时,关注的是reflect.StringHeader,而StringHeader中Data和Len两个字段一个是uintptr,一个是int,在64位机器上各占8个字节,所以返回的是16。

1package main
2import (
3	"fmt"
4	"unsafe"
5)
6func main() {
7	s := "hello world"
8	fmt.Println(unsafe.Sizeof(s)) // 16
9}
© 2024 青蛙小白