C++|Golang 语言怎么高效使用字符串?( 二 )


字符串拼接
在 Golang 语言中 , 关于字符串拼接有多种方式 , 分别是:

  • 使用操作符
  • +/+=
  • 使用
  • fmt.Sprintf
  • 使用
  • bytes.Buffer
  • 使用
  • strings.Join
  • 使用
  • strings.Builder
其中使用操作符是最易用的 , 但是它不是最高效的 , 一般使用场景是用于已知需要拼接的字符串的长度 。
使用  拼接字符串 , 性能是最差的 , 但是它可以格式化 , 所以一般使用场景是需要格式化拼接字符串 。
fmt.Sprintf
使用  和使用  的性能比较接近 , 性能最高的字符串拼接方式是使用  。
bytes.Buffer
strings.Join
strings.Builder
我准备对  的字符串拼接方式多费些笔墨 。
strings.Builder
Golang 语言标准库 strings 中的 Builder 类型 , 用于在 Write 方法中有效拼接字符串 , 它减少了数据拷贝和内存分配 。

Builder 结构体中包含两个字段 , 分别是 addr 和 buf , 字段 addr 是指针类型 , 字段 buf 是字节切片类型 , 但是它的值仍然不允许被修改 , 但是字节切片中的值可以被拼接或者被重置 。
Builder 提供了一系列 Write* 拼接方法 , 这些方法可以用于把新数据拼接到已存在的数据的末尾 , 同时如果字节切片的容量不够用 , 可以自动扩容 。 需要注意的是 , 只要触发扩容 , 就会涉及内存分配和数据拷贝 。 自动扩容规则和切片的扩容规则相同 。
除了自动扩容 , 还可以手动扩容 , Builder 提供的 Grow 方法 , 可以根据 int 类型的传参 , 扩充字节数量 。 因为扩容操作 , 会涉及内存分配和数据拷贝 , 所以调用 Grow 方法手动扩容时 , Golang 也做了优化 , 如果当前字节切片的容量剩余字节数小于或等于传参的值 ,Grow 方法将不会执行扩容操作 。 手动扩容规则是原字节切片容量的 2 倍加上传参的值 。
Builder 类型还提供了一个重置方法 Reset , 它可以将 Builder 类型的变量重置为零值 。 被重置后 , 原字节切片将会被垃圾回收 。
在了解完上述 Builder 的介绍后 , 相信读者已对 Builder 有了初步认识 。 下面我们通过代码看一下预分配字节数量和未分配字节数量的区别:


阅读上面这段代码 , 可以发现调用 Grow 方法 , 预分配字节数量比未预分配字节数量的字符串拼接效率高 。 我们在可以预估字节数量的前提下 , 尽量使用 Grow 方法预先分配字节数量 。

  • 注意:第一 , Builder 类型的变量在被调用之后 , 不可以再被复制 , 否则会引发 panic 。 第二 , 因为 Builder 类型的值不是完全不可修改的 , 所以使用者需要注意并发安全的问题 。
字符串和字节切片互相转换
因为切片类型除了只能和 nil 做比较之外 , 切片类型之间是无法做比较操作的 。 如果我们需要对切片类型做比较操作 , 通常的做法是先将切片类型转换为字符串类型 。 但是因为 string 类型是只读的 , 不可修改的 , 所以转换操作会涉及内存分配和数据拷贝 。
为了提升转换的性能 , 唯一的方法就是减少或者避免内存分配的开销 。 在 Golang 语言中 , 运行时对二者的互相转换也做了优化 , 感兴趣的读者可以阅读 runtime 中的相关源码:
/usr/local/go/src/runtime/string.go
但是 , 我们还可以继续优化 , 实现零拷贝的转换操作 , 从而避免内存分配的开销 , 提升转换效率 。