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

从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了:

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

上面的代码在第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。

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