前面在学习Go程序进程的内存布局时,分析了一个Go程序在Linux下ELF文件格式经过简化后大致如下图所示:
Go二进制文件ELF主要有这几个section组成: .text , .rodata , .data , .noptrdata , .bss , .noptrbss 。 .rodata 中存放的是常量数据,程序中的字面量在编译时会被放到这个section中,那么字符串字面量也将放到这个section中。 在执行程序时,ELF文件被加载到内存中,相同权限的Section会被对应到一个Segment中,大致是下图的内存加载结构:
程序在运行时字符串字面量在内存结构中的代码区。本文将学习Go语言中string的内部数据结构。
string的内部数据结构
string在Go中的内部结构是 reflect.StringHeader 位于 reflect/value.go 。
// 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中 unsafe.Pointer 表示任意类型的指针,可以将一个string的地址显式转换成 unsafe.Pointer ,就可以进一步显式转换成 *reflect.StringHeader 了:
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。
package main
import (
"fmt"
"unsafe"
)
func main() {
s := "hello world"
fmt.Println(unsafe.Sizeof(s)) // 16
}