您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息

一文了解golang slice和string的重用

2025/7/5 10:40:58发布23次查看
golang slice 和 string 重用相比于 c/c++,golang 的一个很大的改进就是引入了 gc 机制,不再需要用户自己管理内存,大大减少了程序由于内存泄露而引入的 bug,但是同时 gc 也带来了额外的性能开销,有时甚至会因为使用不当,导致 gc 成为性能瓶颈,所以 golang 程序设计的时候,应特别注意对象的重用,以减少 gc 的压力。而 slice 和 string 是 golang 的基本类型,了解这些基本类型的内部机制,有助于我们更好地重用这些对象
slice 和 string 内部结构slice 和 string 的内部结构可以在 $goroot/src/reflect/value.go 里面找到
type stringheader struct {    data uintptr    len  int}type sliceheader struct {    data uintptr    len  int    cap  int}
可以看到一个 string 包含一个数据指针和一个长度,长度是不可变的
slice 包含一个数据指针、一个长度和一个容量,当容量不够时会重新申请新的内存,data 指针将指向新的地址,原来的地址空间将被释放
从这些结构就可以看出,string 和 slice 的赋值,包括当做参数传递,和自定义的结构体一样,都仅仅是 data 指针的浅拷贝
slice 重用append 操作si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}si2 := si1si2 = append(si2, 0)convey(重新分配内存, func() {    header1 := (*reflect.sliceheader)(unsafe.pointer(&si1))    header2 := (*reflect.sliceheader)(unsafe.pointer(&si2))    fmt.println(header1.data)    fmt.println(header2.data)    so(header1.data, shouldnotequal, header2.data)})
si1 和 si2 开始都指向同一个数组,当对 si2 执行 append 操作时,由于原来的 cap 值不够了,需要重新申请新的空间,因此 data 值发生了变化,在 $goroot/src/reflect/value.go 这个文件里面还有关于新的 cap 值的策略,在 grow 这个函数里面,当 cap 小于 1024 的时候,是成倍的增长,超过的时候,每次增长 25%,而这种内存增长不仅仅数据拷贝(从旧的地址拷贝到新的地址)需要消耗额外的性能,旧地址内存的释放对 gc 也会造成额外的负担,所以如果能够知道数据的长度的情况下,尽量使用 make([]int, len, cap) 预分配内存,不知道长度的情况下,可以考虑下面的内存重用的方法
内存重用si1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}si2 := si1[:7]convey(不重新分配内存, func() {    header1 := (*reflect.sliceheader)(unsafe.pointer(&si1))    header2 := (*reflect.sliceheader)(unsafe.pointer(&si2))    fmt.println(header1.data)    fmt.println(header2.data)    so(header1.data, shouldequal, header2.data)})convey(往切片里面 append 一个值, func() {    si2 = append(si2, 10)    convey(改变了原 slice 的值, func() {        header1 := (*reflect.sliceheader)(unsafe.pointer(&si1))        header2 := (*reflect.sliceheader)(unsafe.pointer(&si2))        fmt.println(header1.data)        fmt.println(header2.data)        so(header1.data, shouldequal, header2.data)        so(si1[7], shouldequal, 10)    })})
si2 是 si1 的一个切片,从第一段代码可以看到切片并不重新分配内存,si2 和 si1 的 data 指针指向同一片地址,而第二段代码可以看出,当我们往 si2 里面 append 一个新的值的时候,我们发现仍然没有内存分配,而且这个操作使得 si1 的值也发生了改变,因为两者本就是指向同一片 data 区域,利用这个特性,我们只需要让 si1 = si1[:0] 就可以不断地清空 si1 的内容,实现内存的复用了
ps: 你可以使用 copy(si2, si1) 实现深拷贝
stringconvey(字符串常量, func() {    str1 := hello world    str2 := hello world    convey(地址相同, func() {        header1 := (*reflect.stringheader)(unsafe.pointer(&str1))        header2 := (*reflect.stringheader)(unsafe.pointer(&str2))        fmt.println(header1.data)        fmt.println(header2.data)        so(header1.data, shouldequal, header2.data)    })})
这个例子比较简单,字符串常量使用的是同一片地址区域
convey(相同字符串的不同子串, func() {    str1 := hello world[:6]    str2 := hello world[:5]    convey(地址相同, func() {        header1 := (*reflect.stringheader)(unsafe.pointer(&str1))        header2 := (*reflect.stringheader)(unsafe.pointer(&str2))        fmt.println(header1.data, str1)        fmt.println(header2.data, str2)        so(str1, shouldnotequal, str2)        so(header1.data, shouldequal, header2.data)    })})
相同字符串的不同子串,不会额外申请新的内存,但是要注意的是这里的相同字符串,指的是 str1.data == str2.data && str1.len == str2.len,而不是 str1 == str2,下面这个例子可以说明 str1 == str2 但是其 data 并不相同
convey(不同字符串的相同子串, func() {    str1 := hello world[:5]    str2 := hello golang[:5]    convey(地址不同, func() {        header1 := (*reflect.stringheader)(unsafe.pointer(&str1))        header2 := (*reflect.stringheader)(unsafe.pointer(&str2))        fmt.println(header1.data, str1)        fmt.println(header2.data, str2)        so(str1, shouldequal, str2)        so(header1.data, shouldnotequal, header2.data)    })})
实际上对于字符串,你只需要记住一点,字符串是不可变的,任何字符串的操作都不会申请额外的内存(对于仅内部数据指针而言),我曾自作聪明地设计了一个 cache 去存储字符串,以减少重复字符串所占用的空间,事实上,除非这个字符串本身就是由 []byte 创建而来,否则,这个字符串本身就是另一个字符串的子串(比如通过 strings.split 获得的字符串),本来就不会申请额外的空间,这么做简直就是多此一举。
更多golang相关技术文章,请访问golang教程栏目!
以上就是一文了解golang slice和string的重用的详细内容。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录 Product