string底层实现原理与使用

134 阅读4分钟

这篇博客写得很全面了,我补充一些知识点。

查看Go代码的汇编

为了深入了解Go代码执行时的行为,我们可以使用工具来查看生成的汇编代码。这有助于我们判断调用了标准库的哪个函数,例如参考博客中提到的gostringnocopy。

# 方法一:先编译出二进制文件,再反汇编
go build -o main .
go tool objdump -S main > main.s

# 方法二:直接编译为汇编文件
go build -gcflags=-S -o output.s main.go
通过上述命令,我们可以分析程序运行时的实际行为,包括哪些底层函数被调用了。
  • 1个遗留疑问
package main

/*
#include <stdlib.h>
*/
import "C"
import (
   "fmt"
   "unsafe"
)

func main() {
   s1 := string(make([]byte, 1<<20)) // 1MB
   s0 := string(s1[:50])
   fmt.Println(s0)
   var str string
   str = "Hello"
   fmt.Println(str)
   // CGO 示例:使用 gostringnocopy
   cstr := C.CString("Hello from C")
   defer C.free(unsafe.Pointer(cstr))
}

疑问:我上面的代码包含3种字符串初始化:字面值初始化和字节数组初始化,c语言字符串初始化。

只能看到用了以下标准库代码。没有看到参考博客中提到的gostringnocopy的调用

runtime.makeslice(SB)
runtime.slicebytetostring(SB)
$go:string."Hello from C"(SB), R0
$go:string."Hello"(SB), R0

理解以下几个操作的几个方面

  1. 内存拷贝情况
  2. 是否共享底层数组。
  3. 是否可能导致内存泄漏。
  • []byte和string互转,都会涉及1次内存拷贝。

    这是因为string是不可变的,而[]byte则是可变的; 因此,必须创建新的内存区域来存放转换后的数据,以保证原始数据的安全性和一致性。

  • 如何修改字符串,修改原理

  • 字符串拼接

  • 字符串截取: 截取带中文字符串

  • Q: 为什么字符串不允许修改?&& 字符串的参数传递

    1. 在go实现中,string不包含内存空间,只有一个内存的地址,这样做的好处是string变得非常轻量,可以很方便的进行传递而不用担心内存拷贝。

      所以字符串参数传递,只是传递一个视图,不存在底层数组的拷贝,高效。

    2. string通常指向字符串字面量,而字符串字面量存储存储位置是只读段,而不是堆或栈上,所以string不可修改。

    3. 修改字符串时,可以将字符串转换为 []byte 进行修改。

本篇补充几个case

case 1 字符串切片共享底层数组,意味着截取任意子串都不会分配新的底层数组
  • 字符串切片共享底层数组: s0 := s1[:50] 或者 s0 := string(s1[:50]),都不会产生内存分配。 可以benchmark或者runtime.ReadMemStats进行验证。 它只是产生一个新的字符串头,修改其中的len字段,但是data字段不用修改,都指向同样的底层数组。

    这个知识点和字符串的不可变性,我觉得有点矛盾。因为不可变肯定说明是拷贝了一份,这样才不会导致修改底层数组影响多个字符串。

    我猜测: go是把字符存放在只读段,这样就没人可以修改到底层数组,从保持不可变。然后如果是截取左半边字符串,只需要调整len就行。但是如果截取中间s1[:50][20:]或者右边字符s1[20:],那估计还是要产生新的底层数组才行,因为只调整len产生不了这类子串。

  • 推翻了上述的猜测:Benchmark验证了截取中间,右边的子串,都没有内存分配。

    原因:因为字符串头的data和len字段都可以调整,这样新的字符串头,可以表示任意子串。

    结论:无论是截取左边、中间还是右边的子串,Go 字符串切片的操作方式都是相同的,不会分配新的底层数组。

case 2

注意 字符串切片 与 []byte与string互转,这是三种操作,后2个操作总是有内存分配的。

case 3 编译器的某种优化

Q:s = string([]byte(s1[:500]))理论上这个操作涉及2次内存分配(string->[]byte->string),或者(字符串切片(子串)->[]byte->string)。

A: 但是benchmark测试,每次只分配512B(字节),也就是只有1次内存分配(应该前半截string->[]byte)。

case 4 IDE的错误提示
  • s = string([]byte(s1[:500]))编译器提示去掉 []byte 是因为语义等价。但是去掉后底层的内存分配就不同了,会被作为内存切片不再分配。去掉 []byte又会再提示可以去掉string,最后编码会是s = s1[:500],这种就是共享底层数组的字符串切片,具有内存泄漏风险,如果s不释放,那么s1的底层数组(比如1MB)都会泄漏,不是只泄漏500或者512B

case 4 相关内存泄漏资料: