go 字符串

go 字符串

2023/04/09

相关文档:

0. 关于 rune

标识 Unicode 码点(code point) 的整数值, 其类型声明为: type rune = int32 是 int32 的别名。

1. base

  • 不可变
  • zeroed value: ""
  • 底层数据结构 reflect.StringHeader:
    type StringHeader struct {
        // Data 是一个指针, 指向一个字节数组
    	Data uintptr
    	Len  int
    }
    可以发现其只比 SliceHeader 少了 Cap, 因为其自身不可变所以并不需要, 不可用 cap 函数. [] 取值, for…range 与 slice 完全相同. 依然需要注意 [] 会引用原数据, 直接使用将使原数据将无法 gc
  • 传递的是 header, 指向同一字节数组, 但是 string 本身是不可修改的

string 同 slice 一样传递的是 header, 所以不会发生拷贝. 下面示例可以看到底层数组地址相同.

String interning

go 有字符串驻留优化, 字符串常量或内容相同的字符串的底层数组相同

2. 与 []byte/rune 转换

string 转 []byte/rune:

  • []rune(s) 会复制
  • []byte(s) 如果没有更改, 则会被编译器优化成零拷贝(zero-copy string->[]byte conversion), internal/escape

[]byte/rune 转 string: 始终复制

[]byte 转换零拷贝

原理:

  • string 的底层结构只比 slice 少了一个 cap, 元素容器都是都是指针指向的数组, 所以转换时丢弃或者补充 cap 即可

在标准库中就有类似写法, 如 strings.Builder#String(), 源码:

func (b *Builder) String() string {
	return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}

gin 等框架也广泛使用。

func StringToBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

相关函数:

  • unsafe.SliceData: 获取 slice 的底层数组指针
  • unsafe.String: 组装 string, 参数 ptr 是 []byte 的指针, len 是长度
  • unsafe.StringData: 获取 string 的底层数据指针
  • unsafe.Slice: 组装 slice, 参数 ptr 是 []byte 的指针, len 是长度, cap 会取 len

修改底层字节数组

  • slice 的底层数组构建 string, slice 可以修改,并且也会同时反映到 string, 再次拿出 string 的字节数组也是可修改的. 所以 rawstringtmp 不会报错
  • string 原本的底层数组不可修改, 利用反射复用同一数组的 slice 也不可改(修改会导致 fatal error, 此种情况无法被 recover ! 类似例子还有 map 的并发读写 / channel 死锁 / 内存或线程耗尽 …)

3. 拼接

go compiler 调试的设置: (测试发现自己程序不能使用外部包, 否则会 could not import xxx (file not found))

  • debugger package: cmd/compile
  • program args: E:\0-project\dev-go-web\main.go

关于 + 拼接:

  1. 编译器会将 + 转为 OADDSTR 而调用 walkAddString(walkExpr)
  2. walkAddString 会根据操作数(此处操作数已经是合并左侧常量后的, 比如 "a" + "b" + varStr 会直接优化成 "ab" + varStr)的数量做优化:
    1. 如果操作数小于等于 5, 则调用对应的 concatstring%d, 然后调用 concatstrings(buf, []string{操作数})
    2. 否则创建一个 slice 存储所有字符串, 调用 concatstrings
  3. concatstring 相关函数可以 debug 通过上步的 typecheck.LookupRuntime(fn) 的返回值的 sym.Pkg 看到位于 runtime 下, 具体位置是 runtime/string.go#L25 (runtime 下 debug 入口是自己程序而不是 compile). 其主要步骤是:
    1. 遍历操作数计算拼接的长度 l, 以及非空操作数数量 count, 最后一个非空的位置 idx
    2. count == 0 则直接返回空串; count == 1: If there is just one string and either it is not on the stack or our result does not escape the calling frame (buf != nil), then we can return that string directly.
    3. rawstringtmp 准备存储空间, s 是结果字符串, b 是 s 对应的底层字节数据. 关于此字符串的字节数组可修改见下面 修改底层字节数组
    4. 遍历操作数 copy 到 b(源码 copy(b, x)), copy 之后 b 需要移动到下个位置开始(源码 b = b[len(x):])
Last updated on