字符串拼接操作及性能测试报告
❝
可以遗憾,但不要后悔。
我们留在这里,从来不是身不由己。
——— 而是选择在这里经历生活
❞
目录
本文主要围绕
Golang的字符串拼接操作所展开,介绍了其基本用法和各种方式的基准性能测试报告。
拼接操作
💡 在 Go 中,可以使用以下三种方式进行字符串的拼接操作。
使用 + 运算符
可以使用 + 运算符将两个字符串连接起来,如:
str1 := "Hello"
str2 := "world"
result := str1 + " " + str2 // "Hello world"
注意,这种方法可能会导致内存分配问题和效率问题,特别是在循环中进行大量字符串拼接操作时。
使用 strings.Join() 函数
可以使用 strings.Join() 函数将多个字符串拼接起来,如:
strs := []string{"Hello", "world"}
result := strings.Join(strs, " ") // "Hello world"
这种方法比使用 + 运算符更高效,因为它使用了一个内置的缓冲区来拼接字符串。
使用 bytes.Buffer 类型
可以使用 bytes.Buffer 类型来动态构建一个字符串,如:
var buffer bytes.Buffer
buffer.WriteString("Hello")
buffer.WriteString(" ")
buffer.WriteString("world")
result := buffer.String() // "Hello world"
这种方法也比使用 + 运算符更高效,因为它使用了一个内置的缓冲区来拼接字符串,并避免了内存分配问题。
基准测试
测试目标
💡 Go 中字符串拼接的操作方式性能对比。
测试方案
在测试函数中,我们使用 testing.B 类型的 N 字段来指定基准测试的次数,这里使用 b.N 表示让 Go 运行基准测试足够长的时间(通常为 10s),并根据实际情况动态调整基准测试次数,以获取更准确的测试结果。
| 序号 | 描述 | 方式 | 代码复杂度 |
|---|---|---|---|
| 方式1 | + 运算符 | + | 简单,友好 |
| 方式2 | Join 方法 | strings.Join() | 适中,还凑活 |
| 方式3 | Buffer 缓冲 | bytes.Buffer.WriteString() | 繁琐,不太友好 |
测试运行
- 代码
package test
import (
"bytes"
"testing"
)
// BenchmarkStringJoinWithPlus 测试使用 "+" 运算符拼接字符串的性能
func BenchmarkStringJoinWithPlus(b *testing.B) {
str1 := "Hello"
str2 := "world"
for i := 0; i < b.N; i++ {
result := str1 + " " + str2
_ = result // 避免编译器优化代码
}
}
// BenchmarkStringJoinWithBuffer 测试使用 bytes.Buffer 拼接字符串的性能
func BenchmarkStringJoinWithBuffer(b *testing.B) {
str1 := "Hello"
str2 := "world"
for i := 0; i < b.N; i++ {
var buffer bytes.Buffer
buffer.WriteString(str1)
buffer.WriteString(" ")
buffer.WriteString(str2)
result := buffer.String()
_ = result // 避免编译器优化代码
}
}
- 命令
$ go test -bench=. -benchmem
其中,-bench=. 表示运行所有的基准测试函数,-benchmem 表示在测试结果中包含内存分配的信息。
测试结果
- 2 个字符串拼接
| 方法 | 10s 内执行次数 | 执行效率 | 排名 |
|---|---|---|---|
+ | 452651973 | 25.04 ns/op | 中等 |
strings.Join() | 610302502 | 20.06 ns/op | 最高 |
bytes.Buffer.WriteString() | 328923414 | 36.97 ns/op | 最低 |
- 3 个字符串拼接
| 方法 | 10s 内执行次数 | 执行效率 | 排名 |
|---|---|---|---|
+ | 260910256 | 46.60 ns/op | 最低 |
strings.Join() | 446207451 | 26.79 ns/op | 最高 |
bytes.Buffer.WriteString() | 287801275 | 41.04 ns/op | 中等 |
- 4 个字符串拼接
| 方法 | 10s 内执行次数 | 执行效率 | 排名 |
|---|---|---|---|
+ | 171983340 | 69.46 ns/op | 最低 |
strings.Join() | 359583722 | 33.38 ns/op | 最高 |
bytes.Buffer.WriteString() | 257206122 | 46.97 ns/op | 中等 |
- 5 个字符串拼接
| 方法 | 10s 内执行次数 | 执行效率 | 排名 |
|---|---|---|---|
+ | 143962130 | 83.85 ns/op | 最低 |
strings.Join() | 299061463 | 40.68 ns/op | 最高 |
bytes.Buffer.WriteString() | 234218299 | 51.32 ns/op | 中等 |
- 10 个字符串拼接
| 方法 | 10s 内执行次数 | 执行效率 | 排名 |
|---|---|---|---|
+ | 60857520 | 183.7 ns/op | 最低 |
strings.Join() | 160062832 | 75.31 ns/op | 中等 |
bytes.Buffer.WriteString() | 170295568 | 72.36 ns/op | 最高 |
- 15 个字符串拼接
| 方法 | 10s 内执行次数 | 执行效率 | 排名 |
|---|---|---|---|
+ | 37362598 | 317.6 ns/op | 最低 |
strings.Join() | 100000000 | 109.5 ns/op | 中等 |
bytes.Buffer.WriteString() | 126793352 | 96.11 ns/op | 最高 |
- 30 个字符串拼接
| 方法 | 10s 内执行次数 | 执行效率 | 排名 |
|---|---|---|---|
+ | 16936318 | 676.7 ns/op | 最低 |
strings.Join() | 52884830 | 222.3 ns/op | 中等 |
bytes.Buffer.WriteString() | 75862920 | 155.0 ns/op | 最高 |
- 50 个字符串拼接
| 方法 | 10s 内执行次数 | 执行效率 | 排名 |
|---|---|---|---|
+ | 10547450 | 1146 ns/op | 最低 |
strings.Join() | 32108361 | 377.0 ns/op | 中等 |
bytes.Buffer.WriteString() | 51436773 | 236.8 ns/op | 最高 |
- 100 个字符串拼接
| 方法 | 10s 内执行次数 | 执行效率 | 排名 |
|---|---|---|---|
+ | 4827876 | 2454 ns/op | 最低 |
strings.Join() | 16298336 | 752.6 ns/op | 中等 |
bytes.Buffer.WriteString() | 25483066 | 473.2 ns/op | 最高 |
测试结论
💡 不要有思维定势,技术说到底,如何取舍,两 "害" 取其轻。
常规项目使用建议:
- 当
2个或最多3个字符串拼接时,使用+号,操作简单,虽然性能差,可舍弃一些极致的性能。 - 当超过
2个以上,< 50个字符串拼接时,建议使用strings.Join()方法,性能很高,操作同样方便。 - 虽然性能最高,通常不建议,操作相对繁琐,除非
> 50个字符串的拼接,buffer方式性能优势明显。
如果项目性能要求非常高,那自行判断取舍吧。
高性能方案
💡 用 Go 还是要学着适应自己造轮子,可以使用 strings.Builder 类型来拼接字符串,它是 Go 1.10 引入的类型,专门用于高效地构建字符串。
一个简单的封装
package test
import (
"strings"
"testing"
)
// 可接受任意多个字符串拼接
func JoinStrings(strs ...string) string {
var builder strings.Builder
for _, s := range strs {
builder.WriteString(s)
}
return builder.String()
}
func TestJoinString(t *testing.T) {
result := utils.JoinStrings("Hello", " ", "world")
fmt.Println(result)
}
终极字符串化器
package test
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"testing"
)
// 可接受任意多个覆盖大部分类型的转换为字符串的终极拼接
func UltimateStringifier(values ...interface{}) string {
var builder strings.Builder
for _, v := range values {
switch v := v.(type) {
case string:
builder.WriteString(v)
case int:
builder.WriteString(strconv.Itoa(v))
case byte:
builder.WriteByte(v)
case rune:
builder.WriteRune(v)
case bool:
builder.WriteString(strconv.FormatBool(v))
case float32:
builder.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 32))
case float64:
builder.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
case []string:
for _, s := range v {
builder.WriteString(s)
}
case []int:
for _, i := range v {
builder.WriteString(strconv.Itoa(i))
}
case []byte:
builder.Write(v)
default:
// check if v implements the fmt.Stringer interface
if str, ok := v.(fmt.Stringer); ok {
builder.WriteString(str.String())
} else {
// try to marshal v into JSON
b, err := json.Marshal(v)
if err == nil {
builder.Write(b)
} else {
// fallback to reflect.Value.String()
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr && rv.IsNil() {
builder.WriteString("<nil>")
} else {
builder.WriteString(rv.String())
}
}
}
}
}
return builder.String()
}
func TestUltimateConcatenatedString(t *testing.T) {
v1 := []string{"Hello", " ", "world", "-"}
v2 := []byte{'A', 'B', 'C'}
v3 := []int{1, 2, 3}
v4 := '😊'
v5 := 6.66
v6 := false
v7 := 10000
v8 := "ohh~"
v9 := map[string]interface{}{"k1": 1, "k2": "d", "k3": true}
v10 := (*interface{})(nil)
result := UltimateStringifier(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)
fmt.Println(result)
}