GoLang strings.Builder底層實現(xiàn)方法詳解
1.strings.Builder結(jié)構(gòu)體
1.1strings.Builder結(jié)構(gòu)體
// A Builder is used to efficiently build a string using Write methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
1.2Write方法
// 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...)
return len(p), nil
}
1.3WriteByte方法
// WriteByte appends the byte c to b's buffer.
// The returned error is always nil.
func (b *Builder) WriteByte(c byte) error {
b.copyCheck()
b.buf = append(b.buf, c)
return nil
}
1.4WriteRune方法
// WriteRune appends the UTF-8 encoding of Unicode code point r to b's buffer.
// It returns the length of r and a nil error.
func (b *Builder) WriteRune(r rune) (int, error) {
b.copyCheck()
// Compare as uint32 to correctly handle negative runes.
if uint32(r) < utf8.RuneSelf {
b.buf = append(b.buf, byte(r))
return 1, nil
}
l := len(b.buf)
if cap(b.buf)-l < utf8.UTFMax {
b.grow(utf8.UTFMax)
}
n := utf8.EncodeRune(b.buf[l:l+utf8.UTFMax], r)
b.buf = b.buf[:l+n]
return n, nil
}
1.5.WriteString方法
// WriteString appends the contents of s to b's buffer.
// It returns the length of s and a nil error.
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
1.6String方法
和 bytes.Buffer一樣,strings.Builder 也支持使用 String() 來獲取最終的字符串結(jié)果。為了節(jié)省內(nèi)存分配,它通過使用指針技術(shù)將內(nèi)部的 buffer bytes 轉(zhuǎn)換為字符串。所以 String() 方法在轉(zhuǎn)換的時候節(jié)省了時間和空間。
// String returns the accumulated string.
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
1.7Len方法
// Len returns the number of accumulated bytes; b.Len() == len(b.String()).
func (b *Builder) Len() int { return len(b.buf) }
1.8Cap方法
// Cap returns the capacity of the builder's underlying byte slice. It is the
// total space allocated for the string being built and includes any bytes
// already written.
func (b *Builder) Cap() int { return cap(b.buf) }
1.9Reset方法
// Reset resets the Builder to be empty.
func (b *Builder) Reset() {
b.addr = nil
b.buf = nil
}
1.10Grow方法
// Grow grows b's capacity, if necessary, to guarantee space for
// another n bytes. After Grow(n), at least n bytes can be written to b
// without another allocation. If n is negative, Grow panics.
func (b *Builder) Grow(n int) {
b.copyCheck()
if n < 0 {
panic("strings.Builder.Grow: negative count")
}
if cap(b.buf)-len(b.buf) < n {
b.grow(n)
}
}
1.11grow方法
// grow copies the buffer to a new, larger buffer so that there are at least n
// bytes of capacity beyond len(b.buf).
func (b *Builder) grow(n int) {
buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
copy(buf, b.buf)
b.buf = buf
}
1.12copyCheck方法
func (b *Builder) copyCheck() {
if b.addr == nil {
// 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")
}
}
2.strings.Builder介紹
與 bytes.Buffer 類似,strings.Builder 也支持 4 類方法將數(shù)據(jù)寫入 builder 中。
func (b *Builder) Write(p []byte) (int, error)
func (b *Builder) WriteByte(c byte) error
func (b *Builder) WriteRune(r rune) (int, error)
func (b *Builder) WriteString(s string) (int, error)
有了它們,用戶可以根據(jù)輸入數(shù)據(jù)的不同類型(byte 數(shù)組,byte, rune 或者 string),選擇對應(yīng)的寫入方法。
3.存儲原理
根據(jù)用法說明,我們通過調(diào)用 string.Builder 的寫入方法來寫入內(nèi)容,然后通過調(diào)用 String() 方法來獲取拼接的字符串。那么 string.Builder 是如何組織這些內(nèi)容的呢?
通過 slice,string.Builder 通過使用一個內(nèi)部的 slice 來存儲數(shù)據(jù)片段。當(dāng)開發(fā)者調(diào)用寫入方法的時候,數(shù)據(jù)實際上是被追加(append)到了其內(nèi)部的 slice 上。
4.拷貝問題
strings.Builder 不推薦被拷貝。當(dāng)你試圖拷貝 strings.Builder 并寫入的時候,你的程序就會崩潰。
你已經(jīng)知道,strings.Builder 內(nèi)部通過 slice 來保存和管理內(nèi)容。slice 內(nèi)部則是通過一個指針指向?qū)嶋H保存內(nèi)容的數(shù)組。 當(dāng)我們拷貝了 builder 以后,同樣也拷貝了其 slice 的指針。但是它仍然指向同一個舊的數(shù)組。當(dāng)你對源 builder 或者拷貝后的 builder 寫入的時候,問題就產(chǎn)生了。另一個 builder 指向的數(shù)組內(nèi)容也被改變了。這就是為什么 strings.Builder 不允許拷貝的原因。
func main() {
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
b2.WriteString("DEF")//出錯在這一行,panic: strings: illegal use of non-zero Builder copied by value
}
func main() {
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
fmt.Println(b2.String())//ABC
}
func main() {
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
fmt.Println(b1.String()) //輸出:ABC
fmt.Println(b2.String()) //輸出:ABC
b1.WriteString("DEF")
fmt.Println(b1.String()) //輸出:ABCDEF
fmt.Println(b2.String()) //輸出:ABC
}
但對于一個未寫入任何東西的空內(nèi)容 builder 則是個例外。我們可以拷貝空內(nèi)容的 builder 而不報錯。
func main() {
var b1 strings.Builder
b2 := b1
fmt.Println(b1.String()) //輸出空行
fmt.Println(b2.String()) //輸出空行
b2.WriteString("DEF")
fmt.Println(b1.String()) //輸出空行
fmt.Println(b2.String()) //輸出:DEF
b1.WriteString("ABC")
fmt.Println(b1.String()) //輸出:ABC
fmt.Println(b2.String()) //輸出:DEF
}
strings.Builder 會在以下方法中檢測拷貝操作:
Grow(n int)
Write(p []byte)
WriteRune(r rune)
WriteString(s string)
所以,拷貝并使用下列這些方法是允許的:
func main() {
// Reset()
// Len()
// String()
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
fmt.Println(b2.Len()) // 3
fmt.Println(b2.String()) // ABC
b2.Reset()
b2.WriteString("DEF")
fmt.Println(b2.String()) // DEF
}
5.不能與nil作比較

6.Grow深入
strings.Builder 是通過其內(nèi)部的 slice 來儲存內(nèi)容的。當(dāng)你調(diào)用寫入方法的時候,新的字節(jié)數(shù)據(jù)就被追加到 slice 上。如果達(dá)到了 slice 的容量(capacity)限制,一個新的 slice 就會被分配,然后老的 slice 上的內(nèi)容會被拷貝到新的 slice 上。當(dāng) slice 長度很大時,這個操作就會很消耗資源甚至引起 內(nèi)存問題。我們需要避免這一情況。
關(guān)于 slice,Go 語言提供了 make([]TypeOfSlice, length, capacity) 方法在初始化的時候預(yù)定義它的容量。這就避免了因達(dá)到最大容量而引起擴容。
strings.Builder 同樣也提供了 Grow() 來支持預(yù)定義容量。當(dāng)我們可以預(yù)定義我們需要使用的容量時,strings.Builder 就能避免擴容而創(chuàng)建新的 slice 了。
當(dāng)調(diào)用 Grow() 時,我們必須定義要擴容的字節(jié)數(shù)(n)。 Grow() 方法保證了其內(nèi)部的 slice 一定能夠?qū)懭?n 個字節(jié)。只有當(dāng) slice 空余空間不足以寫入 n 個字節(jié)時,擴容才有可能發(fā)生。
舉個例子:
builder 內(nèi)部 slice 容量為 10。
builder 內(nèi)部 slice 長度為 5。
當(dāng)我們調(diào)用 Grow(3) => 擴容操作并不會發(fā)生。因為當(dāng)前的空余空間為 5,足以提供 3 個字節(jié)的寫入。
當(dāng)我們調(diào)用 Grow(7) => 擴容操作發(fā)生。因為當(dāng)前的空余空間為 5,已不足以提供 7 個字節(jié)的寫入。
關(guān)于上面的情形,如果這時我們調(diào)用 Grow(7),則擴容之后的實際容量是多少?
17 還是 12?
實際上,是 27。strings.Builder 的 Grow() 方法是通過 current_capacity * 2 + n (n 就是你想要擴充的容量)的方式來對內(nèi)部的 slice 進行擴容的。所以說最后的容量是 10*2+7 = 27。 當(dāng)你預(yù)定義 strings.Builder 容量的時候還要注意一點。調(diào)用 WriteRune() 和 WriteString() 時,rune 和 string 的字符可能不止 1 個字節(jié)。因為,你懂的,UTF-8 的原因。
func main() {
var b1 strings.Builder
fmt.Println(b1.Len()) //0
fmt.Println(b1.Cap()) //0
b1.Grow(3)
fmt.Println(b1.Len()) //0
fmt.Println(b1.Cap()) //3
b1.Grow(1)
fmt.Println(b1.Len()) //0
fmt.Println(b1.Cap()) //3
}
func main() {
a := strings.Builder{}
a.Grow(11)
fmt.Println(a.Len()) //0
fmt.Println(a.Cap()) //11
a.WriteRune('李')
a.WriteRune('陸')
a.WriteRune('豪')
a.WriteRune('Z')
a.WriteRune('Z')
fmt.Println(a.Len()) //11
fmt.Println(a.Cap()) //11
}
7.不支持并行讀寫
和 bytes.Buffer 一樣,strings.Builder 也不支持并行的讀或者寫。所以我們們要稍加注意。
可以試一下,通過同時給 strings.Builder 添加 1000 個字符:
通過運行,你會得到不同長度的結(jié)果。但它們都不到 1000。
func main() {
var b strings.Builder
n := 0
var wait sync.WaitGroup
for n < 1000 {
wait.Add(1)
go func() {
b.WriteString("1")
n++
wait.Done()
}()
}
wait.Wait()
fmt.Println(len(b.String()))
/*
第一次運行輸出:946
第二次運行輸出:933
第三次運行輸出:900
*/
}
到此這篇關(guān)于GoLang strings.Builder底層實現(xiàn)方法詳解的文章就介紹到這了,更多相關(guān)GoLang strings.Builder內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang關(guān)鍵字select的常用用法總結(jié)
這篇文章主要為大家詳細(xì)介紹了golang中select關(guān)鍵字的常用用法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-10-10
Golang基于epoll實現(xiàn)最簡單網(wǎng)絡(luò)通信框架
這篇文章主要為大家詳細(xì)介紹了Golang如何基于epoll實現(xiàn)最簡單網(wǎng)絡(luò)通信框架,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)學(xué)習(xí)2023-06-06

