前言
因为之前是搞java的在使用字符串拼接的时候避免一直+
产生新的String对象,就使用StringBuilder或者StringBuffer。最近在使用go的时候也需要字符串拼接,所以查了下go的字符串拼接的方式,发现有很多种,下面我们将从使用->直观的性能比较->背后的原因来娓娓道来。
使用
fmt.Sprintf
func useFmtSprintf(s1 string, s2 string) {
fmt.Println(fmt.Sprintf("%s-%s", s1, s2))
}
+
func useAdd(s1 string, s2 string) {
fmt.Println(s1 + s2)
}
strings.Join
func useStringJoin(s1 string, s2 string) {
fmt.Println(strings.Join([]string{s1, s2}, ""))
}
bytes.Buffer
func useBuffer(s1 string, s2 string) {
buffer := bytes.Buffer{}
buffer.WriteString(s1)
buffer.WriteString(s2)
fmt.Println(buffer.String())
}
strings.Builder
func useStringBuilder(s1 string, s2 string) {
builder := strings.Builder{}
builder.WriteString(s1)
builder.WriteString(s2)
fmt.Println(builder.String())
}
性能对比
package test
import (
"bytes"
"fmt"
"strings"
"testing"
)
// fmt.Printf
func BenchmarkFmtSprintfMore(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
s += fmt.Sprintf("%s%s", "hello", "world")
}
fmt.Errorf(s)
}
// 加号 拼接
func BenchmarkAddMore(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
s += "hello" + "world"
}
fmt.Errorf(s)
}
// strings.Join
func BenchmarkStringsJoinMore(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
s += strings.Join([]string{"hello", "world"}, "")
}
fmt.Errorf(s)
}
// bytes.Buffer
func BenchmarkBufferMore(b *testing.B) {
buffer := bytes.Buffer{}
for i := 0; i < b.N; i++ {
buffer.WriteString("hello")
buffer.WriteString("world")
}
fmt.Errorf(buffer.String())
}
func BenchmarkStringBuilderMore(b *testing.B) {
builder := strings.Builder{}
for i := 0; i < b.N; i++ {
builder.WriteString("hello")
builder.WriteString("world")
}
fmt.Errorf(builder.String())
}
结果
➜ test go test -bench="." -count=3
goos: darwin
goarch: arm64
pkg: StudyProject/src/second/test
BenchmarkFmtSprintfMore-8 235827 70838 ns/op
BenchmarkFmtSprintfMore-8 403614 102891 ns/op
BenchmarkFmtSprintfMore-8 388071 103072 ns/op
BenchmarkAddMore-8 413247 132776 ns/op
BenchmarkAddMore-8 412111 129470 ns/op
BenchmarkAddMore-8 403748 127128 ns/op
BenchmarkStringsJoinMore-8 404487 113118 ns/op
BenchmarkStringsJoinMore-8 392866 111663 ns/op
BenchmarkStringsJoinMore-8 399028 112117 ns/op
BenchmarkBufferMore-8 77135485 17.27 ns/op
BenchmarkBufferMore-8 87218017 17.58 ns/op
BenchmarkBufferMore-8 85368238 14.25 ns/op
BenchmarkStringBuilderMore-8 92404837 13.56 ns/op
BenchmarkStringBuilderMore-8 94131186 14.14 ns/op
BenchmarkStringBuilderMore-8 92947599 13.59 ns/op
PASS
ok StudyProject/src/second/test 400.951s
结论
使用strings.Builder的效果最好,当然如果是平时使用少量的拼接笔者还是会使用+
。下面我们来看看为啥差距这么大。
背后原因
因为上面的压测结果,前面三种都差不多,所以都归位+
,后面的Buffer和StringBuilder都是用到了buffer来优化,但是性能还是有稍许差别,所以下面分为两类,为啥+
性能那么差,为啥bytes.Buffer
和strings.Builder
有稍许区别。
+
性能那么差
差的原因在于每次+都要两个string复制到新分配的string中,每次循环都要随着需要拼接后的字符串越长需要重新分配的内存越大,同样需要销毁的空间也越来越大。
为啥bytes.Buffer
和strings.Builder
有稍许区别
buffer的扩充算法不一样。代码还涉及内建函数,我们可以通过单测来看看扩充的不同:
func TestBuilderConcat(t *testing.T) {
var str = "1"
var builder strings.Builder
cap := 0
for i := 0; i < 10000; i++ {
if builder.Cap() != cap {
fmt.Print(builder.Cap(), " ")
cap = builder.Cap()
}
builder.WriteString(str)
}
}
func TestBufferConcat(t *testing.T) {
var str = "1"
buffer := bytes.Buffer{}
cap := 0
for i := 0; i < 10000; i++ {
if buffer.Cap() != cap {
fmt.Print(buffer.Cap(), " ")
cap = buffer.Cap()
}
buffer.WriteString(str)
}
}
输出:
➜ test go test -run="TestBufferConcat" . -v
=== RUN TestBufferConcat
64 129 259 519 1039 2079 4159 8319 16639 --- PASS: TestBufferConcat (0.00s)
PASS
ok StudyProject/src/second/test 0.321s
➜ test go test -run="TestBuilderConcat" . -v
=== RUN TestBuilderConcat
8 16 32 64 128 256 512 896 1408 2048 3072 4096 5376 6912 9472 12288 --- PASS: TestBuilderConcat (0.00s)
PASS
ok StudyProject/src/second/test 0.235s
bytes.Buffer
无脑2n+1,strings.Builder
前期会2n到了512之后就不会翻倍扩充。
番外-测试
testing包提供了自动测试的支持。文件名需要以_test.go结尾。测试文件和被测试文件放在一起不会被build,想要build测试文件可以使用“go test”命令。更多的信息,可以执行"go help test" 和 "go help testflag"查看。
基准测试
func BenchmarkXxx(*testing.B)
上面func格式被认为是一个基准测试,可以使用 go test -bench
按顺序执行。
func BenchmarkRandInt(b *testing.B) {
for i := 0; i < b.N; i++ {
rand.Int()
}
}
基准方法是被执行b.N次。在基准测试执行期间,b.N会被调整,直到基准测试函数持续的时间足够长以至耗时稳定。结果如下:
➜ test go test -bench="."
goos: darwin
goarch: arm64
pkg: StudyProject/src/second/test
BenchmarkRandInt-8 85999309 13.48 ns/op
PASS
ok StudyProject/src/second/test 2.175s
Examples
使用注释提供输出结果和结果进行比较进行测试。(笔者觉得这个有点太随意了,很容易出错。)
有序的:
func ExampleHello() {
fmt.Println("hello")
fmt.Println("goodbye")
// Output:
// hello,and
// goodbye
}
输出:
➜ test go test -run="ExampleHello" -v
=== RUN ExampleHello
--- FAIL: ExampleHello (0.00s)
got:
hello
goodbye
want:
hello,and
goodbye
FAIL
exit status 1
FAIL StudyProject/src/second/test 0.408s
无序的:
func ExamplePerm() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// Unordered output: 4
// 2
// 1
// 3
// 0
}
输出:
➜ test go test -run="ExamplePerm" -v
testing: warning: no tests to run
PASS
ok StudyProject/src/second/test 0.391s
➜ test go test -run="ExamplePerm" -v
=== RUN ExamplePerm
--- PASS: ExamplePerm (0.00s)
PASS
ok StudyProject/src/second/test 0.303s
Fuzzing
Fuzzing是一种自动化的测试技术,它不断的创建输入用来测试程序的bug。Go fuzzing使用覆盖率智能指导遍历被模糊测试的代码,发现缺陷并报告给用户。由于模糊测试可以达到人类经常忽略的边缘场景,因此它对于发现安全漏洞和缺陷特别有价值。
下面是一个模糊测试的示例,突出标识了它的主要组件。
func FuzzHex(f *testing.F) {
for _, seed := range [][]byte{{}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, in []byte) {
enc := hex.EncodeToString(in)
out, err := hex.DecodeString(enc)
if err != nil {
t.Fatalf("%v: decode: %v", in, err)
}
if !bytes.Equal(in, out) {
t.Fatalf("%v: not equal after round trip: %v", in, out)
}
})
}
输出:
➜ test go test -run="FuzzHex" -v
=== RUN FuzzHex
=== RUN FuzzHex/seed#0
=== RUN FuzzHex/seed#1
=== RUN FuzzHex/seed#2
=== RUN FuzzHex/seed#3
=== RUN FuzzHex/seed#4
--- PASS: FuzzHex (0.00s)
--- PASS: FuzzHex/seed#0 (0.00s)
--- PASS: FuzzHex/seed#1 (0.00s)
--- PASS: FuzzHex/seed#2 (0.00s)
--- PASS: FuzzHex/seed#3 (0.00s)
--- PASS: FuzzHex/seed#4 (0.00s)
PASS
ok StudyProject/src/second/test 0.693s
参考
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。