我们一般使用字符串拼接方式有三种
str += "sum"
我们先来写一个benchmark去测试每一种字符串拼接的情况。
// 1. 直接拼接
func BenchmarkString(b *testing.B) {
elems := make([]string, 100000, 100000)
for i := 0; i < len(elems); i++ {
elems[i] = strconv.Itoa(i)
}
sum := ""
length := len(elems)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < length; j++ {
sum += elems[j]
}
}
b.StopTimer()
}
// 2. fmt.Sprintf("%s",xxxxx)
func BenchmarkFmtSprintfString(b *testing.B) {
elems := make([]string, 100000, 100000)
for i := 0; i < len(elems); i++ {
elems[i] = strconv.Itoa(i)
}
length := len(elems)
sum := ""
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < length; j++ {
sum += fmt.Sprintf("%s", elems[j])
}
}
b.StopTimer()
}
// 3. string.Builder
func BenchmarkBuilderString(b *testing.B) {
elems := make([]string, 100000, 100000)
for i := 0; i < len(elems); i++ {
elems[i] = strconv.Itoa(i)
}
var builder strings.Builder
length := len(elems)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < length; j++ {
builder.WriteString(elems[j])
}
}
b.StopTimer()
}
// 4. bytes.Builder
func BenchmarkByteBufferString(b *testing.B) {
elems := make([]string, 100000, 100000)
for i := 0; i < len(elems); i++ {
elems[i] = strconv.Itoa(i)
}
buffer := new(bytes.Buffer)
length := len(elems)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < length; j++ {
buffer.WriteString(elems[j])
}
}
b.StopTimer()
}
// 5. byte 拼接
func BenchmarkByteConcatString(b *testing.B) {
elems := make([]string, 100000, 100000)
for i := 0; i < len(elems); i++ {
elems[i] = strconv.Itoa(i)
}
length := len(elems)
buf := make([]byte, 0, len(elems))
b.ResetTimer()
for i := 0; i < b.N; i++ {
for j := 0; j < length; j++ {
buf = append(buf, elems[j]...)
}
}
b.StopTimer()
}
- 执行
go test -bench="String$" -benchmem .
or
gobench string_test.go
- 结果
测试函数 同等时间内执行了多少次 总共的执行时间
1. 直接拼接
BenchmarkString
BenchmarkString-8 1 8930752200 ns/op
2. fmt.Sprintf
BenchmarkFmtSprintf
BenchmarkFmtSprintf-8 1 8773251900 ns/op
3. StringBuilder
BenchmarkBuilder
BenchmarkBuilder-8 991 1199075 ns/op
4. ByteBuffer
BenchmarkByteBuffer
BenchmarkByteBuffer-8 1458 945317 ns/op
5. ByteConcat
BenchmarkByteConcat
BenchmarkByteConcat-8 2010 803761 ns/op
我们一个一个来进行分析
第一个:
不断开辟内存空间
字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接两个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和。拼接第三个字符串时,再开辟一段新空间,新空间大小是三个字符串大小之和,以此类推。
假设一个字符串大小为 10 byte,拼接 1w 次,需要申请的内存大小为:
10 + 2 * 10 + 3 * 10 + … + 10000 * 10 byte = 500 MB
fmt.Sprintf
在 Go 里面的反射是这样设计的:
type_ := reflect.TypeOf(obj)
field, _ := type_.FieldByName("fan")
reflect.StructField
type_ := reflect.ValueOf(obj)
value := type_.FieldByName("fan")
reflect.Valuemallocreflect.ValueGC
当我们涉及到了大量的内存开辟的时候,就会使得系统变得异常慢。
第三个:
strings.Builderbytes.Buffer[]byte
例如:
20byte > 16 byte30 byte < 32 byte
所以内存的复用是性能极高的!
第四个:
[]byte 数组bytes.Bufferstrings.Builder[]byte
第五个:
而第五个,毫无疑问事先分配好了内存,肯定是最快的了。
参考链接Go语言的反射原理
Go语言的string拼接