Go 字符串拼接最佳實踐

字符串是一個常見的數(shù)據(jù)類型,在 Go 語言在內(nèi)的很多語言中,為了安全,都把字符串設計為不可變。每生成一個字符串都是在創(chuàng)建一個新的字符串,而不是在原有字符串的基礎(chǔ)上修改。

在 Go 中,字符串拼接的方式很多,可以直接使用 +,也可以使用 fmt.SPrintf,還可以使用 strings.Builder 和 bytes.Buffer。

在這篇文章中,來討論一下在代碼中如何做字符串拼接效率最好。

1. 做一個基準測試

在開始分析每種拼接方法的優(yōu)劣之前,先跑一個簡單的基準測試,來看一下每種字符串拼接方法的性能。

Go 中提供了基準測試框架,測試文件需要以 test 結(jié)尾,然后每個測試方法以 Benchmark 開頭,這次對加號、fmt.SPrintf、和 strings.Builder 三種方式進行基準測試,代碼如下:

func BenchmarkPlus(b *testing.B) {
    str := "this is just a string"

    for i := 0; i < b.N; i++ {
        stringPlus(str)
    }
}

func BenchmarkSPrintf(b *testing.B) {
    str := "this is just a string"
    for i := 0; i < b.N; i++ {
        stringSprintf(str)
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    str := "this is just a string"
    for i := 0; i < b.N; i++ {
        stringBuilder(str)
    }
}

func stringPlus(str string) string {
    s := ""
    for i := 0; i < 10000; i++ {
        s += str
    }
    return s
}

func stringSprintf(str string) string {
    s := ""
    for i :=0; i < 10000; i++ {
        s += str
    }
    return s
}

func stringBuilder(str string) string {
    builder := strings.Builder{}
    for i := 0; i < 100000; i++ {
        builder.WriteString(str)
    }
    return builder.String()
}

基準測試需要使用 *testing.B ,其中 b.N 不是一個固定的值,這個值的大小由框架自己來決定。

在這里,我們分別測試用不同的方式拼接一個固定的字符串 10000 次,然后統(tǒng)計平均的代碼執(zhí)行時間,內(nèi)存消耗情況。使用如下的命令運行基準測試:

go test -bench=. -benchmem

-bench=. 參數(shù)運行當前包中所有基準測試,-benchmem 表示對測試的內(nèi)存使用情況進行統(tǒng)計。運行上面的命令之后,輸出結(jié)果如下:

goos: darwin
goarch: amd64
pkg: zxin.com/zx-demo/string_benchmark
BenchmarkPlus-12                      12          96586447 ns/op        1086401355 B/op    10057 allocs/op
BenchmarkSPrintf-12                   12          97037216 ns/op        1086402698 B/op    10065 allocs/op
BenchmarkStringBuilder-12            655           1713353 ns/op        11671537 B/op         35 allocs/op
PASS
ok      zxin.com/zx-demo/string_benchmark       6.186s

第一列表示基準測試的方法名稱和所用的 GOMAXPROCS 的值,第二列表示這次測試循環(huán)的次數(shù),第三列表示平均每次測試所用的時間,單位為納秒,第四列表示平均每次運行所分配的內(nèi)存,第五列表示每次運行所分配內(nèi)存的次數(shù)。

通過上面的測試,可以發(fā)現(xiàn) strings.Builder 的表現(xiàn)是最好的,比直接使用加號來拼接字符串的內(nèi)存消耗要小 100 倍。

2. 為什么性能的差異這么大

通過上面的基準測試可以發(fā)現(xiàn),使用不同的方式來拼接字符串,性能差異很大。

Go 的字符串是不可變的,如果使用加號的方式來拼接字符串,那么每次拼接都需要重新分配內(nèi)存。而 strings.Builder 會對內(nèi)存預分配,在字符串不斷寫入的過程中,會自動擴容長度。

strings.Builder 的底層存儲使用的是 []byte,初始的長度分配是 32,然后每次擴容時都會翻一倍。

type Builder struct {
    addr *Builder
    buf  []byte
}

當長度到大 2048 時,再擴容就不會直接翻倍,而是每次增加 640 的倍數(shù),第一次增加 640,第二次增加 1280,以此類推。

在大量拼接字符串的時候 strings.Builder 會比直接拼接的效率更高。

bytes.Buffer 是另一個類似的庫,與 strings.Builder 性能相當,但如果是對于純拼接字符串的場景,還是推薦使用 strings.Builder。

3. 拼字符串的最佳實踐

雖然 strings.Builder 的性能很高,但并不是所有的場景都是合這個。如果只是一次簡單的字符串拼接,直接使用加號就夠了。

如果涉及到一些字符串的格式化,那么使用 fmt.Sprintf 就更合適了。

那么在大量拼接字符串的場景,直接使用 strings.Builder 就完事了么,其實還可以繼續(xù)優(yōu)化一下。在使用 strings.Builder 時,如果字符串在不斷的增加,底層的存儲還是要不斷的擴容。如果可以預估字符串的長度,就可以提前分配好內(nèi)存。減少擴容的次數(shù)。

增加一個測試用例:

func BenchmarkStringBuilderPre(b *testing.B) {
    str := "this is just a string"
    for i := 0; i < b.N; i++ {
        stringBuilderPre(str)
    }
}

func stringBuilderPre(str string) string {
    builder := strings.Builder{}
    builder.Grow(1000000)
    for i := 0; i < 100000; i++ {
        builder.WriteString(str)
    }
    return builder.String()
}

下面是基準測試的結(jié)果:

pkg: zxin.com/zx-demo/string_benchmark
BenchmarkPlus-12                              12          96676019 ns/op        1086401676 B/op    10057 allocs/op
BenchmarkSPrintf-12                           12          96693407 ns/op        1086402022 B/op    10058 allocs/op
BenchmarkStringBuilder-12                    607           1822282 ns/op        11671543 B/op         35 allocs/op
BenchmarkStringBuilderPre-12                 860           1393689 ns/op         8257539 B/op          5 allocs/op

可以看到,在提前指定長度的情況下,性能又提升了不少,內(nèi)存的占用量和分配次數(shù)下降了不少,運行時間也有所提升。

文 / Rayjun

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 1 字典 map是一種較為特殊的數(shù)據(jù)結(jié)構(gòu),在任何一種編程語言中都可以看見他的身影,它是一種鍵值對結(jié)構(gòu),通過給定的k...
    泥人冷風閱讀 398評論 0 0
  • GO字符串拼接 1) +運算符 2)fmt.Sprintf函數(shù) 3) bytes.Buffer 4) ...
    木工007閱讀 301評論 0 1
  • (一)加號拼接 這種拼接最簡單,也最容易被我們使用,在編程過程我們幾乎下意識就是使用+好進行拼接。 (二)使用fm...
    L白水飄萍閱讀 462評論 0 1
  • 背景介紹 在我們實際開發(fā)過程中,不可避免的要進行一些字符串的拼接工作。比如將一個數(shù)組按照一定的標點符號拼接成一個句...
    JankinHou閱讀 754評論 0 0
  • 先上代碼 運行結(jié)果 如果還不知道 Go 的 benchmark,可以先去了解一下,個人認為還是非常不錯的性能測試的...
    leejnull閱讀 3,666評論 0 0

友情鏈接更多精彩內(nèi)容