只会用+拼接字符串吗?你该重学一下Go了!
在go语言中,字符串是一种不可变的字节序列。字符串拼接是指将两个或多个字符串合并成一个新的字符串的过程。go语言提供了多种字符串拼接的方式,本文将介绍其中的几种,并进行基准测试比较。
一、直接使用+进行拼接
这是最简单和直观的方法,只需要使用+运算符将两个或多个字符串相加即可。例如:
s1 := "Hello"
s2 := "World"
s3 := s1 + " " + s2 // s3 = "Hello World"
这种方法的优点是易于理解和使用,不需要引入额外的包或函数。但是,这种方法的缺点是效率低下,因为每次拼接都会创建一个新的字符串,分配新的内存空间,并复制原来的字符串内容。如果需要拼接很多字符串,或者在循环中进行拼接,这种方法会造成大量的内存开销和性能损失。
二、strings.Builder
这是一种高效和推荐的方法,它使用了strings包中的Builder类型。Builder是一个可变的字节缓冲区,可以用来构建字符串。它提供了WriteString方法,可以将一个字符串追加到Builder中。最后,可以使用String方法将Builder转换为一个不可变的字符串。例如:
var b strings.Builder
b.WriteString("Hello")
b.WriteString(" ")
b.WriteString("World")
s := b.String() // s = "Hello World"
这种方法的优点是效率高,因为它避免了不必要的内存分配和复制。它也比较简洁和易用,只需要调用两个方法即可。但是,这种方法的缺点是需要引入strings包,并且不能直接使用+运算符进行拼接。
三、bytes.Buffer
这是一种类似于strings.Builder的方法,它使用了bytes包中的Buffer类型。Buffer也是一个可变的字节缓冲区,可以用来构建字符串。它提供了WriteString方法,可以将一个字符串追加到Buffer中。最后,可以使用String方法将Buffer转换为一个不可变的字符串。例如:
var b bytes.Buffer
b.WriteString("Hello")
b.WriteString(" ")
b.WriteString("World")
s := b.String() // s = "Hello World"
这种方法和strings.Builder非常相似,优缺点也基本一致。不过,bytes.Buffer还提供了其他一些方法,比如WriteByte、WriteRune、WriteTo等,可以更灵活地操作字节。
四、fmt.Sprintf
这是一种格式化和惯用的方法,它使用了fmt包中的Sprintf函数。Sprintf函数可以根据一个模板和一些参数生成一个格式化的字符串,并返回该字符串。例如:
s1 := "Hello"
s2 := "World"
s3 := fmt.Sprintf("%s %s", s1, s2) // s3 = "Hello World"
这种方法的优点是清晰和易读,可以根据需要生成各种格式的字符串,并且可以处理不同类型的参数,比如int、bool等,而不需要显式转换。但是,这种方法的缺点是效率低下,因为它涉及到反射、格式解析、内存分配等操作。而且,这种方法不适合在不知道参数数量和类型的情况下进行拼接。
五、append []byte
这是一种直接操作字节切片的方法,它使用了内置的append函数。append函数可以将一个或多个元素追加到一个切片中,并返回该切片。如果要拼接字符串,可以先将字符串转换为字节切片,然后使用append函数进行拼接,最后再将字节切片转换为字符串。例如:
s1 := "Hello"
s2 := "World"
b := []byte(s1)
b = append(b, ' ')
b = append(b, s2...)
s3 := string(b) // s3 = "Hello World"
这种方法的优点是效率高,因为它直接操作字节,避免了额外的内存分配和复制。但是,这种方法的缺点是不够简洁和易用,需要进行多次类型转换,并且不能直接使用+运算符进行拼接。
六、各种方式的基准测试比较
为了比较各种方式的性能,我们可以使用testing包中的Benchmark函数进行基准测试。下面是一个简单的测试代码,用来比较拼接10个字符串的时间和内存消耗:
package main
import (
"bytes"
"fmt"
"strings"
"testing"
)
var strs = []string{"This", "is", "a", "benchmark", "test", "for", "string", "concatenation", "in", "Go"}
func BenchmarkPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for _, str := range strs {
s += str
}
}
}
func BenchmarkBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var b strings.Builder
for _, str := range strs {
b.WriteString(str)
}
b.String()
}
}
func BenchmarkBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
var b bytes.Buffer
for _, str := range strs {
b.WriteString(str)
}
b.String()
}
}
func BenchmarkSprintf(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("%s%s%s%s%s%s%s%s%s%s", strs[0], strs[1], strs[2], strs[3], strs[4], strs[5], strs[6], strs[7], strs[8], strs[9])
}
}
func BenchmarkAppend(b *testing.B) {
for i := 0; i < b.N; i++ {
var b []byte
for _, str := range strs {
b = append(b, str...)
}
string(b)
}
}
运行该测试代码,可以得到以下结果:
goos: darwin
goarch: arm64
pkg: test2/str
BenchmarkPlus-8 5765235 203.1 ns/op 256 B/op 9 allocs/op
BenchmarkBuilder-8 11304490 105.5 ns/op 120 B/op 4 allocs/op
BenchmarkBuffer-8 15438054 74.89 ns/op 112 B/op 2 allocs/op
BenchmarkSprintf-8 3949538 302.5 ns/op 208 B/op 11 allocs/op
BenchmarkAppend-8 10571956 115.2 ns/op 168 B/op 5 allocs/op
PASS
ok test2/str 6.866s
从结果可以看出:
- Sprintf是性能最差的,直接使用+进行拼接性能也很差;
- bytes.Buffer内存开辟次数、大小、耗时、每秒运行次数都是最好的
- strings.Builder和Append性能也还不错,一般情况下也不会形成差距
当然,这只是一个简单的测试,不同的情况结果可能会有所不同,取决于字符串的数量、长度、内容等因素。所以,在选择字符串拼接的方法时,还需要根据具体的场景和需求进行权衡和测试。实际应用中,使用strings.Builder等已经能满足我们的实际需求。在某些特殊情况下,在追求极致性能的时候,我们还可以通过与分配内存、底层数据转换等方式获取更好的性能,后面有机会再进行详细的讲述。
#go##golang项目##goalng##go项目##golang研发实习生#