在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
性能参考上文