Golang中拼接字符串的6種方式性能對(duì)比
golang的string類型是不可修改的,對(duì)于拼接字符串來(lái)說(shuō),本質(zhì)上還是創(chuàng)建一個(gè)新的對(duì)象將數(shù)據(jù)放進(jìn)去。主要有以下幾種拼接方式
拼接方式介紹
1.使用string自帶的運(yùn)算符+
ans = ans + s
2. 使用格式化輸出fmt.Sprintf
ans = fmt.Sprintf("%s%s", ans, s)
3. 使用strings的join函數(shù)
一般適用于將字符串?dāng)?shù)組轉(zhuǎn)化為特定間隔符的字符串的情況
ans=strings.join(strs,",")
4. 使用strings.Builder
builder := strings.Builder{} builder.WriteString(s) return builder.String()
5. 使用bytes.Buffer
buffer := new(bytes.Buffer) buffer.WriteString(s) return buffer.String()
6. 使用[]byte,并且提前設(shè)置容量
ans := make([]byte, 0, len(s)*n) ans = append(ans, s...)
性能對(duì)比
先寫一個(gè)隨機(jī)生成長(zhǎng)度為n的字符串的函數(shù)
func getRandomString(n int) string { var tmp = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ans := make([]uint8, 0, n) for i := 0; i < n; i++ { ans = append(ans, tmp[rand.Intn(len(tmp))]) } return string(ans) }
接下來(lái)分別寫出上述拼接方式的實(shí)現(xiàn),假設(shè)每次都拼接n次字符串s后返回。
1.使用string自帶的運(yùn)算符+
循環(huán)n次,每次都令答案字符串a(chǎn)ns+源字符串s
func plusOperatorJoin(n int, s string) string { var ans string for i := 0; i < n; i++ { ans = ans + s } return ans }
2. 使用格式化輸出fmt.Sprintf
循環(huán)n次,使用fmt.Sprintf達(dá)到拼接的目的
func sprintfJoin(n int, s string) string { var ans string for i := 0; i < n; i++ { ans = fmt.Sprintf("%s%s", ans, s) } return ans }
3. 使用strings的join函數(shù)
拼接同一個(gè)字符串的話不適合用join函數(shù),所以跳過(guò)這種方式
4. 使用strings.Builder
初始化strings.Builder,循環(huán)n次,每次調(diào)用WriteString方法
func stringBuilderJoin(n int, s string) string { builder := strings.Builder{} for i := 0; i < n; i++ { builder.WriteString(s) } return builder.String() }
5. 使用bytes.Buffer
初始化bytes.Buffer,循環(huán)n次,每次調(diào)用WriteString方法
func bytesBufferJoin(n int, s string) string { buffer := new(bytes.Buffer) for i := 0; i < n; i++ { buffer.WriteString(s) } return buffer.String() }
6. 使用[]byte,并且提前設(shè)置容量
定義ans為byte數(shù)組,并提前設(shè)置容量為len(s)∗n
func bytesJoin(n int, s string) string { ans := make([]byte, 0, len(s)*n) for i := 0; i < n; i++ { ans = append(ans, s...) } return string(ans) }
測(cè)試代碼
先隨機(jī)生成一個(gè)長(zhǎng)度為10的字符串,然后拼接10000次。
package high_strings import "testing" func benchmark(b *testing.B, f func(int, string) string) { var str = getRandomString(10) for i := 0; i < b.N; i++ { f(10000, str) } } func BenchmarkPlusOperatorJoin(b *testing.B) { benchmark(b, plusOperatorJoin) } func BenchmarkSprintfJoin(b *testing.B) { benchmark(b, sprintfJoin) } func BenchmarkStringBuilderJoin(b *testing.B) { benchmark(b, stringBuilderJoin) } func BenchmarkBytesBufferJoin(b *testing.B) { benchmark(b, bytesBufferJoin) } func BenchmarkBytesJoin(b *testing.B) { benchmark(b, bytesJoin) }
測(cè)試結(jié)果
使用[]byte>strings.Builder≥bytes.Buffer>ffmt.Sprintf > +運(yùn)算符
源碼分析
1.使用string自帶的運(yùn)算符+
代碼在runtime\string.go里
// concatstrings implements a Go string concatenation x+y+z+... // The operands are passed in the slice a. // If buf != nil, the compiler has determined that the result does not // escape the calling function, so the string data can be stored in buf // if small enough. func concatstrings(buf *tmpBuf, a []string) string { idx := 0 l := 0 count := 0 for i, x := range a { n := len(x) if n == 0 { continue } if l+n < l { throw("string concatenation too long") } l += n count++ idx = i } if count == 0 { return "" } ??????? // If there is just one string and either it is not on the stack // or our result does not escape the calling frame (buf != nil), // then we can return that string directly. if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) { return a[idx] } s, b := rawstringtmp(buf, l) for _, x := range a { copy(b, x) b = b[len(x):] } return s }
首先計(jì)算拼接后的字符串長(zhǎng)度
如果只有一個(gè)字符串并且不在棧上就直接返回
如果buf不為空并且buf可以放下這些字符串,就把拼接后的字符串放在buf里,否則在堆上重新申請(qǐng)一塊內(nèi)存
func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) { if buf != nil && l <= len(buf) { b = buf[:l] s = slicebytetostringtmp(&b[0], len(b)) } else { s, b = rawstring(l) } return } // rawstring allocates storage for a new string. The returned // string and byte slice both refer to the same storage. // The storage is not zeroed. Callers should use // b to set the string contents and then drop b. func rawstring(size int) (s string, b []byte) { p := mallocgc(uintptr(size), nil, false) return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size) }
然后遍歷數(shù)組,將字符串copy過(guò)去
2. 使用strings.Builder
介紹:strings.Builder用于使用Write方法高效地生成字符串,它最大限度地減少了內(nèi)存復(fù)制
拼接過(guò)程:builder里有一個(gè)byte類型的切片,每次調(diào)用WriteString的時(shí)候,是直接往該切片里追加字符串。因?yàn)榍衅讓拥臄U(kuò)容機(jī)制是以倍數(shù)申請(qǐng)的,所以對(duì)比1而言,2的內(nèi)存消耗要更少。
**結(jié)果返回:**在返回字符串的String方法里,是將buf數(shù)組轉(zhuǎn)化為字符串直接返回的。
擴(kuò)容機(jī)制: 想要緩沖區(qū)容量增加n個(gè)字節(jié),擴(kuò)容后容量變?yōu)?∗len+n
// 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 } // String returns the accumulated string. func (b *Builder) String() string { return unsafe.String(unsafe.SliceData(b.buf), len(b.buf)) } // 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 } // 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 }
3. 使用bytes.Buffer
介紹bytes.Buffer跟strings.Builder的底層都是byte數(shù)組,區(qū)別在于擴(kuò)容機(jī)制和返回字符串的String方法。
結(jié)果返回: 因?yàn)閎ytes.Buffer實(shí)際上是一個(gè)流式的字節(jié)緩沖區(qū),可以向尾部寫入數(shù)據(jù),也可以讀取頭部的數(shù)據(jù)。所以在返回字符串的String方法里,只返回了緩沖區(qū)里未讀的部分,所以需要重新申請(qǐng)內(nèi)存來(lái)存放返回的結(jié)果。內(nèi)存會(huì)比strings.Builder稍慢一些。
擴(kuò)容機(jī)制: 想要緩沖區(qū)容量至少增加n個(gè)字節(jié),m是未讀的長(zhǎng)度,c是當(dāng)前的容量。
優(yōu)化點(diǎn)在于如果n<=c/2−m,也就是當(dāng)前容量的一半都大于等于現(xiàn)有的內(nèi)容(未讀的字節(jié)數(shù))加上所需要增加的字節(jié)數(shù),就復(fù)用當(dāng)前的數(shù)組,把未讀的內(nèi)容拷貝到頭部去。
We can slide things down instead of allocating a new slice. We only need m+n <= c to slide, but we instead let capacity get twice as large so we don’t spend all our time copying.
我們可以向下滑動(dòng),而不是分配一個(gè)新的切片。我們只需要m+n<=c來(lái)滑動(dòng),但我們讓容量增加了一倍,這樣我們就不會(huì)把所有的時(shí)間都花在復(fù)制上。
否則的話也是2∗len+n的擴(kuò)張
// A Buffer is a variable-sized buffer of bytes with Read and Write methods. // The zero value for Buffer is an empty buffer ready to use. type Buffer struct { buf []byte // contents are the bytes buf[off : len(buf)] off int // read at &buf[off], write at &buf[len(buf)] lastRead readOp // last read operation, so that Unread* can work correctly. } // String returns the contents of the unread portion of the buffer // as a string. If the Buffer is a nil pointer, it returns "<nil>". // // To build strings more efficiently, see the strings.Builder type. func (b *Buffer) String() string { if b == nil { // Special case, useful in debugging. return "<nil>" } return string(b.buf[b.off:]) } // WriteString appends the contents of s to the buffer, growing the buffer as // needed. The return value n is the length of s; err is always nil. If the // buffer becomes too large, WriteString will panic with ErrTooLarge. func (b *Buffer) WriteString(s string) (n int, err error) { b.lastRead = opInvalid m, ok := b.tryGrowByReslice(len(s)) if !ok { m = b.grow(len(s)) } return copy(b.buf[m:], s), nil } ???????// grow grows the buffer to guarantee space for n more bytes. // It returns the index where bytes should be written. // If the buffer can't grow it will panic with ErrTooLarge. func (b *Buffer) grow(n int) int { m := b.Len() // If buffer is empty, reset to recover space. if m == 0 && b.off != 0 { b.Reset() } // Try to grow by means of a reslice. if i, ok := b.tryGrowByReslice(n); ok { return i } if b.buf == nil && n <= smallBufferSize { b.buf = make([]byte, n, smallBufferSize) return 0 } c := cap(b.buf) if n <= c/2-m { // We can slide things down instead of allocating a new // slice. We only need m+n <= c to slide, but // we instead let capacity get twice as large so we // don't spend all our time copying. copy(b.buf, b.buf[b.off:]) } else if c > maxInt-c-n { panic(ErrTooLarge) } else { // Add b.off to account for b.buf[:b.off] being sliced off the front. b.buf = growSlice(b.buf[b.off:], b.off+n) } // Restore b.off and len(b.buf). b.off = 0 b.buf = b.buf[:m+n] return m }
參考:GoLang bytes.Buffer基礎(chǔ)使用方法詳解
到此這篇關(guān)于Golang中拼接字符串的6種方式性能對(duì)比的文章就介紹到這了,更多相關(guān)Go拼接字符串內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang版本升級(jí)的簡(jiǎn)單實(shí)現(xiàn)步驟
個(gè)人感覺(jué)Go在眾多高級(jí)語(yǔ)言中,是在各方面都比較高效的,下面這篇文章主要給大家介紹了關(guān)于golang版本升級(jí)的簡(jiǎn)單實(shí)現(xiàn)步驟,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02Go中的Context實(shí)現(xiàn)原理以及正確使用方式
在 Go 語(yǔ)言中,Context 包是一種非常常用的工具,它被用來(lái)管理 goroutine 之間的通信和取消,本文將深入探討Context 包的基本原理,包括使用場(chǎng)景、原理和一些最佳實(shí)踐,感興趣的小伙伴跟著小編一起來(lái)看看吧2024-11-11細(xì)細(xì)探究Go 泛型generic設(shè)計(jì)
這篇文章主要帶大家細(xì)細(xì)探究了Go 泛型generic設(shè)計(jì)及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04Go實(shí)現(xiàn)mongodb增刪改查工具類的代碼示例
這篇文章主要給大家介紹了關(guān)于Go實(shí)現(xiàn)mongodb增刪改查工具類的相關(guān)資料,MongoDB是一個(gè)NoSQL數(shù)據(jù)庫(kù),它提供了靈活的文檔存儲(chǔ)模型以及強(qiáng)大的查詢和操作功能,需要的朋友可以參考下2023-10-10自己動(dòng)手用Golang實(shí)現(xiàn)約瑟夫環(huán)算法的示例
這篇文章主要介紹了自己動(dòng)手用Golang實(shí)現(xiàn)約瑟夫環(huán)算法的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12Golang測(cè)試func?TestXX(t?*testing.T)的使用詳解
一般Golang中的測(cè)試代碼都以xxx_test.go的樣式,在命名測(cè)試函數(shù)的時(shí)候以Testxx開(kāi)頭,下面給大家介紹Golang測(cè)試func?TestXX(t?*testing.T)的使用,感興趣的朋友跟隨小編一起看看吧2024-08-08go語(yǔ)言實(shí)現(xiàn)字符串與其它類型轉(zhuǎn)換(strconv包)
strconv包是Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)的一部分,主要提供字符串與基本數(shù)據(jù)類型之間的轉(zhuǎn)換功能,使用strconv包可以方便地在不同類型之間進(jìn)行轉(zhuǎn)換,滿足日常編程中的需求,感興趣的可以了解一下2024-10-10GO將mysql?中?decimal?數(shù)據(jù)類型映射到?protobuf的操作方法
這篇文章主要介紹了go如何優(yōu)雅地將?mysql?中?decimal?數(shù)據(jù)類型映射到?protobuf,本文主要展示一下在 protobuf中 float與double的一個(gè)區(qū)別,結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09