Golang strings.Builder 原理解析
背景
在很多場景中,我們都會進行字串拼接操作。
最開始的時候,你可能會使用如下的操作:
package main
func main() {
ss := []string{
"A",
"B",
"C",
}
var str string
for _, s := range ss {
str += s
}
print(str)
}
與許多支援 string 型別的語言一樣,golang 中的 string 型別也是隻讀且不可變的。因此,這種拼接字串的方式會導致大量的 string 建立、銷燬和記憶體分配。如果你拼接的字串比較多的話,這顯然不是一個正確的姿勢。
在 Golang 1.10 以前,你可以使用bytes.Buffer
來優化:
package main
import (
"bytes"
"fmt"
)
func main() {
ss := []string{
"A",
"B",
"C",
}
var b bytes.Buffer
for _, s := range ss {
fmt.Fprint(&b, s)
}
print(b.String())
}
這裡使用 var b bytes.Buffer
存放最終拼接好的字串,一定程度上避免上面 str
每進行一次拼接操作就重新申請新的記憶體空間存放中間字串的問題。
但這裡依然有一個小問題: b.String()
會有一次 []byte -> string
型別轉換。而這個操作是會進行一次記憶體分配和內容拷貝的。
使用 strings.Builder 進行字串拼接
如果你現在已經在使用 golang 1.10, 那麼你還有一個更好的選擇:strings.Builder
:
package main
import (
"fmt"
"strings"
)
func main() {
ss := []string{
"A",
"B",
"C",
}
var b strings.Builder
for _, s := range ss {
fmt.Fprint(&b, s)
}
print(b.String())
}
Golang 官方將strings.Builder
作為一個 feature 引入,想必是有兩把刷子。不信跑個分?簡單來了個 benchmark:
package ts
import (
"bytes"
"fmt"
"strings"
"testing"
)
func BenchmarkBuffer(b *testing.B) {
var buf bytes.Buffer
for i := 0; i < b.N; i++ {
fmt.Fprint(&buf, "ABC")
_ = buf.String()
}
}
func BenchmarkBuilder(b *testing.B) {
var builder strings.Builder
for i := 0; i < b.N; i++ {
fmt.Fprint(&builder, "ABC")
_ = builder.String()
}
}
╰─➤ go test -bench=. -benchmem 2 ↵
goos: darwin
goarch: amd64
pkg: test/ts
BenchmarkBuffer-4 300000 101086 ns/op 604155 B/op 1 allocs/op
BenchmarkBuilder-4 20000000 90.4 ns/op 21 B/op 0 allocs/op
PASS
ok test/ts 32.308s
效能提升感人。要知道諸如 C#, Java 這些自帶 GC 的語言很早就引入了string builder
, Golang 在 1.10 才引入,時機其實不算早,但是巨大的提升終歸沒讓人失望。下面我們看一下標準庫是如何做到的。
strings.Builder 原理解析
strings.Builder
的實現在檔案strings/builder.go中,一共只有 120 行,非常精煉。關鍵程式碼摘選如下:
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte // 1
}
// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
b.copyCheck()
b.buf = append(b.buf, p...) // 2
return len(p), nil
}
// String returns the accumulated string.
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf)) // 3
}
func (b *Builder) copyCheck() {
if b.addr == nil {
// 4
// This hack works around a failing of Go's escape analysis
// that was causing b to escape and be heap allocated.
// See issue 23382.
// TODO: once issue 7921 is fixed, this should be reverted to
// just "b.addr = b".
b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
} else if b.addr != b {
panic("strings: illegal use of non-zero Builder copied by value")
}
}
- 與
byte.Buffer
思路類似,既然 string 在構建過程中會不斷的被銷燬重建,那麼就儘量避免這個問題,底層使用一個buf []byte
來存放字串的內容。 - 對於寫操作,就是簡單的將 byte 寫入到 buf 即可。
- 為了解決
bytes.Buffer.String()
存在的[]byte -> string
型別轉換和記憶體拷貝問題,這裡使用了一個unsafe.Pointer
的存指標轉換操作,實現了直接將buf []byte
轉換為 string 型別,同時避免了記憶體充分配的問題。 - 如果我們自己來實現 strings.Builder, 大部分情況下我們完成前 3 步就覺得大功告成了。但是標準庫做得要更近一步。我們知道 Golang 的堆疊在大部分情況下是不需要開發者關注的,如果能夠在棧上完成的工作逃逸到了堆上,效能就大打折扣了。因此,
copyCheck
加入了一行比較 hack 的程式碼來避免 buf 逃逸到堆上。關於這部分內容,你可以進一步閱讀 Dave Cheney 的關於Go’s hidden #pragmas.
就此止步?
一般 Golang 標準庫中使用的方式都是會逐步被推廣,成為某些場景下的最佳實踐方式。
這裡使用到的*(*string)(unsafe.Pointer(&b.buf))
其實也可以在其他的場景下使用。比如:如何比較string
與[]byte
是否相等而不進行記憶體分配呢?似乎鋪墊寫得太明顯了,大家應該都會寫了,直接給出程式碼吧:
func unsafeEqual(a string, b []byte) bool {
bbp := *(*string)(unsafe.Pointer(&b))
return a == bbp
}
擴充套件閱讀
Originally published on liudanking.com
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- golang bufio解析Golang
- Golang : cobra 包解析Golang
- Golang 流式解析 JsonGolangJSON
- golang select底層原理Golang
- golang reflect 實現原理Golang
- golang如何使用指標靈活操作記憶體?unsafe包原理解析Golang指標記憶體
- Golang map執行緒安全實現及sync.map使用及原理解析。Golang執行緒
- 解析HOT原理
- DNS解析原理DNS
- 細說 Golang 的 JSON 解析GolangJSON
- Golang字串解析成數字Golang字串
- MyBatis原理解析MyBatis
- BlockCanary原理解析BloC
- KonvaJS 原理解析JS
- Flutter原理深度解析Flutter
- InheritWidget原理解析
- EventBus 原理解析
- kafka原理解析Kafka
- ThreadLocal原理深入解析thread
- gpfdist原理解析
- HTTPS原理解析HTTP
- Sentinel 原理-全解析
- Promise原理解析Promise
- cli原理解析
- CAS原理深度解析
- webpack原理解析Web
- Golang Sync.WaitGroup 使用及原理GolangAI
- Golang網路模型netpoll原始碼解析Golang模型原始碼
- golang解析IP到城市jsonRPC服務GolangJSONRPC
- 走進Golang之編譯器原理Golang編譯
- golang實現常用集合原理介紹Golang
- Markdown-it 原理解析
- singleflight 包原理解析
- Netty(DotNetty)原理解析Netty
- Spring Session原理解析SpringSession
- Mobx autorun 原理解析
- NameServer 核心原理解析Server
- Volley的原理解析