string 类型的数据是不可修改的

无论常量还是变量,一旦声明了一个 string 类型的标识符,该标识符所指代的数据在整个程序的生命周期内便无法更改。

这种不可变性是通过内存保护的,底层的数据存储区只能进行只读操作,即使使用 unsafe.Pointer 获取到了底层数据地址,也不能修改。

string 的底层表示

// runtime/string.go
type stringStruct struct {
	str unsafe.Pointer
	len int
}

从这个结构体可以看出,golang 获取 string 的长度为 时间复杂度的操作。

另外 string 也和 slice 类型类似,类似于值类型,所以即使是巨大的 string 作为参数传递也不会有很大开销。

string 以 UTF8 的编码格式存储在内存中

多行字符串

const s = `好雨知时节,当春乃发生。
随风潜入夜,润物细无声。
野径云俱黑,江船火独明。
晓看红湿处,花重锦官城。`

string 构造效率

Rank(Fastest to slowest)multipler
strings.Builder with initial size-
bytes.Buffer with initial size~1.14x
strings.Join~1.16x
bytes.Buffer~1.9x
+ operator~2.1x
strings.Builder~2.2x
fmt.Sprintf~10x

结论

  1. 做了初始化的 strings.Builder, bytes.Bufferstrings.Join 算第一梯队
  2. 未初始化的 bytes.Buffer, strings.Builder+ 操作符算第二梯队,大概慢一倍左右
  3. fmt.Sprintf 最慢,差了近一个数量级

string 转换时需要拷贝

由于 string 类型的不可变性,导致 string 转 []byte 或者 []Rune 时都会需要分配内存再拷贝数据,反之也是。

同时为了效率,Golang 对某些特定转换做了特殊的优化。

第一种是由于 []byte 不可比较,而 string 可以比较,所有经常需要将 []byte 转换为 string 做比较,再下面几种情况下调用 string(b) 不会分配新的内存。

  1. string(b) 用在 map 类型的 key 中
b := []byte{'k', 'e', 'y'}
m := make(map[string]string)
m[string(b)] = "value"
m[[3]string{string(b), "key1", "key2"}] = "value1"
  1. string(b) 用在字符串连接语句中
b := []byte{'t', 'o', 'n', 'y'}
s := "hello " + string(b) + "!"
  1. string(b) 用在字符串比较中
s := "tom"
b := []byte{'t', 'o', 'n', 'y'}
 
if s < string(b) {
    ...
}

第二种情况是在 for _, v := range []byte(str) 的时候,会直接使用 string 底层的字节数组而不会复制拷贝。