在 src/builtin/builtin.go 文件中,有关于 string 的描述
// string is the set of all strings of 8-bit bytes, conventionally but not // necessarily representing UTF-8-encoded text. A string may be empty, but // not nil. Values of string type are immutable. type string string
翻译如下:
1、字符串是所有 8bit 字节的集合,但编码方式不一定是 UTF-8
2、字符串可以为 empty,但不能为 nil,empty 就是一个没有任何字符的空串
3、字符串不可以被修改,所以字符串类型的值是不可变的
字符串的本质是一串字符数组,每个字符根据编码方式不同,可能对应 1 个或多个整数
字符串拼接的方法:
1、strings.Builder
底层存储使用 [] byte,可以预分配内存,自动扩容,大量字符串拼接场景下性能最好,在转换为 string 类型时,不会进行拷贝操作(一般 [] byte 转换为 strings 类型需要一次拷贝操作)
func Test(t *testing.T) { var q strings.Builder // 指定q的类型为string.Builder q.Grow(100) // 为q分配大小为100字节的内存 q.WriteString("hello world") // 往q中写入字符串"hello world" q.WriteString(", sincerely") // 紧接着"hello world",继续往字符串中写入", sincerely" fmt.Println(q.String()) // q.String(),将q转换为string类型 }
运行结果:
=== RUN Test hello world, sincerely --- PASS: Test (0.00s) PASS
2、bytes.Buffer
与 strings.Builder 相似,使用 [] byte 作为底层存储,但是 bytes.Buffer 在转换为字符串时,会重新申请一块内存,而 strings.Builder 直接将底层的 [] byte 转换成字符串类型返回,性能次于 strings.Builder
3、+ 操作符
“+” 在拼接两个字符串时,会开辟一段新的内存空间,新空间的大小是原来两个字符串的大小之和,所以每拼接一次就要开辟一段空间,性能很差。少量字符串拼接时,最方便,性能也最好,不适合大量的字符串拼接操作。
4、append
可以提前预分配内存,如果不分配,程序也会自动扩容
5、fmt.Sprintf
Sprintf 会从临时对象池中获取一个对象,然后进行格式化操作,最后转换为 string,释放对象,实现很复杂,性能也很差,不是专门用于字符串拼接的
6、strings.Join
strings.Join 性能约等于 strings.Builder,在已经字符串 slice 的时候可以使用,否则构建切片时会有性能损耗,不建议使用
Benchmark 性能测试
使用 Benchmark 测试大量字符串拼接操作情况下,性能情况如下
package main import ( "bytes" "fmt" "strings" "testing" ) var loremIpsum = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna." var strSlice = make([]string, LIMIT) const LIMIT = 1000 func init() { for i := 0; i < LIMIT; i++ { strSlice[i] = loremIpsum } } func BenchmarkConcatenationOperator(b *testing.B) { for i := 0; i < b.N; i++ { var q string for _, v := range strSlice { q = q + v } } b.ReportAllocs() } func BenchmarkFmtSprint(b *testing.B) { for i := 0; i < b.N; i++ { var q string for _, v := range strSlice { q = fmt.Sprintf(q, v) } } b.ReportAllocs() } func BenchmarkBytesBuffer(b *testing.B) { for i := 0; i < b.N; i++ { var q bytes.Buffer q.Grow(len(loremIpsum) * len(strSlice)) for _, v := range strSlice { q.WriteString(v) } _ = q.String() } b.ReportAllocs() } func BenchmarkStringBuilder(b *testing.B) { for i := 0; i < b.N; i++ { var q strings.Builder q.Grow(len(loremIpsum) * len(strSlice)) for _, v := range strSlice { q.WriteString(v) } _ = q.String() } b.ReportAllocs() } func BenchmarkAppend(b *testing.B) { for i := 0; i < b.N; i++ { var q []byte for _, v := range strSlice { q = append(q, v...) } _ = string(q) } b.ReportAllocs() } func BenchmarkAppendPreAlloc(b *testing.B) { for i := 0; i < b.N; i++ { q := make([]byte, 0, len(loremIpsum)*len(strSlice)) for _, v := range strSlice { q = append(q, v...) } _ = string(q) } b.ReportAllocs() } func BenchmarkJoin(b *testing.B) { for i := 0; i < b.N; i++ { var q string q = strings.Join(strSlice, " ") _ = q } b.ReportAllocs() }
测试结果为:
goos: windows goarch: amd64 pkg: awesomeProject cpu: 12th Gen Intel(R) Core(TM) i7-12700H BenchmarkConcatenationOperator BenchmarkConcatenationOperator-20 99 11393268 ns/op 106185805 B/op 1006 allocs/op BenchmarkFmtSprint BenchmarkFmtSprint-20 14 73365786 ns/op 212506515 B/op 4944 allocs/op BenchmarkBytesBuffer BenchmarkBytesBuffer-20 25534 47791 ns/op 425986 B/op 2 allocs/op BenchmarkStringBuilder BenchmarkStringBuilder-20 49358 30250 ns/op 212992 B/op 1 allocs/op BenchmarkAppend BenchmarkAppend-20 9388 133586 ns/op 1119601 B/op 22 allocs/op BenchmarkAppendPreAlloc BenchmarkAppendPreAlloc-20 19702 61435 ns/op 425987 B/op 2 allocs/op BenchmarkJoin BenchmarkJoin-20 35522 34449 ns/op 212993 B/op 1 allocs/op PASS
对于大量字符串拼接操作的场景下,性能如下:
string.Builder ≈ string.Join > bytes.buffer > append > “+” > fmt.sprintf
对于 append 方法,在提前分配内存和不提前分配内存的情况下,性能相差一倍有余,所以尽量还是要避免在拼接字符串过程中的内存分配以提高性能
string 面试与分析
1、string 的底层数据结构是怎样的
string 的底层实现是一个结构类型,包含两个字段,一个是指向字节数组([] byte)的指针,一个是字符串的字节长度。[] byte 底层实现也是一个结构类型,包含三个字段,一个是指向字节数组的指针,一个是字节长度,一个是数组容量。
2、字符串可以被修改吗
不可以
3、[] byte 与 string 互相转换会发生拷贝吗
在大多数场景下会发生拷贝,在部分临时只读场景下,可能不会发生拷贝。
举例:
a. 字符串比较:string (ss) == “hello”
b. 字符串拼接:”hello” + string (ss) + “world”
c. 用作查找,比如 map 的 key,val := map [string (ss)]
这些情况都是临时读取使用,不会有一个赋值给另外一个变量,所以没有发生内存拷贝
4、字符串拼接的方式有哪几种,性能如何?
fmt.Sprintf、strings.Builder、bytes.Buffer、“+”、strings.Join、append
性能参考上文