Go語言如何高效的進(jìn)行字符串拼接(6種方式對比分析)
前言
日常業(yè)務(wù)開發(fā)中離不開字符串的拼接操作,不同語言的字符串實(shí)現(xiàn)方式都不同,在
Go語言中就提供了6種方式進(jìn)行字符串拼接,那這幾種拼接方式該如何選擇呢?使用那個更高效呢?本文我們就一起來分析一下。本文使用Go語言版本:1.17.1
string類型
我們首先來了解一下Go語言中string類型的結(jié)構(gòu)定義,先來看一下官方定義:
// string is the set of all strings of 8-bit bytes, conventionally but not // necessarily representing UTF-8-encoded text. A string may be empty, but // not nil. Values of string type are immutable. type string string
string是一個8位字節(jié)的集合,通常但不一定代表UTF-8編碼的文本。string可以為空,但是不能為nil。string的值是不能改變的。
string類型本質(zhì)也是一個結(jié)構(gòu)體,定義如下:
type stringStruct struct {
str unsafe.Pointer
len int
}
stringStruct和slice還是很相似的,str指針指向的是某個數(shù)組的首地址,len代表的就是數(shù)組長度。怎么和slice這么相似,底層指向的也是數(shù)組,是什么數(shù)組呢?我們看看他在實(shí)例化時(shí)調(diào)用的方法:
//go:nosplit
func gostringnocopy(str *byte) string {
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
s := *(*string)(unsafe.Pointer(&ss))
return s
}
入?yún)⑹且粋€byte類型的指針,從這我們可以看出string類型底層是一個byte類型的數(shù)組,所以我們可以畫出這樣一個圖片:

string類型本質(zhì)上就是一個byte類型的數(shù)組,在Go語言中string類型被設(shè)計(jì)為不可變的,不僅是在Go語言,其他語言中string類型也是被設(shè)計(jì)為不可變的,這樣的好處就是:在并發(fā)場景下,我們可以在不加鎖的控制下,多次使用同一字符串,在保證高效共享的情況下而不用擔(dān)心安全問題。
string類型雖然是不能更改的,但是可以被替換,因?yàn)?code>stringStruct中的str指針是可以改變的,只是指針指向的內(nèi)容是不可以改變的,也就說每一個更改字符串,就需要重新分配一次內(nèi)存,之前分配的空間會被gc回收。
關(guān)于string類型的知識點(diǎn)就描述這么多,方便我們后面分析字符串拼接。
字符串拼接的6種方式及原理
原生拼接方式"+"
Go語言原生支持使用+操作符直接對兩個字符串進(jìn)行拼接,使用例子如下:
var s string s += "asong" s += "真帥"
這種方式使用起來最簡單,基本所有語言都有提供這種方式,使用+操作符進(jìn)行拼接時(shí),會對字符串進(jìn)行遍歷,計(jì)算并開辟一個新的空間來存儲原來的兩個字符串。
字符串格式化函數(shù)fmt.Sprintf
Go語言中默認(rèn)使用函數(shù)fmt.Sprintf進(jìn)行字符串格式化,所以也可使用這種方式進(jìn)行字符串拼接:
str := "asong"
str = fmt.Sprintf("%s%s", str, str)
fmt.Sprintf實(shí)現(xiàn)原理主要是使用到了反射,具體源碼分析因?yàn)槠脑蚓筒辉谶@里詳細(xì)分析了,看到反射,就會產(chǎn)生性能的損耗,你們懂得?。?!
Strings.builder
Go語言提供了一個專門操作字符串的庫strings,使用strings.Builder可以進(jìn)行字符串拼接,提供了writeString方法拼接字符串,使用方式如下:
var builder strings.Builder
builder.WriteString("asong")
builder.String()
strings.builder的實(shí)現(xiàn)原理很簡單,結(jié)構(gòu)如下:
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte // 1
}
addr字段主要是做copycheck,buf字段是一個byte類型的切片,這個就是用來存放字符串內(nèi)容的,提供的writeString()方法就是像切片buf中追加數(shù)據(jù):
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
提供的String方法就是將[]]byte轉(zhuǎn)換為string類型,這里為了避免內(nèi)存拷貝的問題,使用了強(qiáng)制轉(zhuǎn)換來避免內(nèi)存拷貝:
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
bytes.Buffer
因?yàn)?code>string類型底層就是一個byte數(shù)組,所以我們就可以Go語言的bytes.Buffer進(jìn)行字符串拼接。bytes.Buffer是一個一個緩沖byte類型的緩沖器,這個緩沖器里存放著都是byte。使用方式如下:
buf := new(bytes.Buffer)
buf.WriteString("asong")
buf.String()
bytes.buffer底層也是一個[]byte切片,結(jié)構(gòu)體如下:
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.
}
因?yàn)?code>bytes.Buffer可以持續(xù)向Buffer尾部寫入數(shù)據(jù),從Buffer頭部讀取數(shù)據(jù),所以off字段用來記錄讀取位置,再利用切片的cap特性來知道寫入位置,這個不是本次的重點(diǎn),重點(diǎn)看一下WriteString方法是如何拼接字符串的:
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
}
切片在創(chuàng)建時(shí)并不會申請內(nèi)存塊,只有在往里寫數(shù)據(jù)時(shí)才會申請,首次申請的大小即為寫入數(shù)據(jù)的大小。如果寫入的數(shù)據(jù)小于64字節(jié),則按64字節(jié)申請。采用動態(tài)擴(kuò)展slice的機(jī)制,字符串追加采用copy的方式將追加的部分拷貝到尾部,copy是內(nèi)置的拷貝函數(shù),可以減少內(nèi)存分配。
但是在將[]byte轉(zhuǎn)換為string類型依舊使用了標(biāo)準(zhǔn)類型,所以會發(fā)生內(nèi)存分配:
func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}
strings.join
Strings.join方法可以將一個string類型的切片拼接成一個字符串,可以定義連接操作符,使用如下:
baseSlice := []string{"asong", "真帥"}
strings.Join(baseSlice, "")
strings.join也是基于strings.builder來實(shí)現(xiàn)的,代碼如下:
func Join(elems []string, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return elems[0]
}
n := len(sep) * (len(elems) - 1)
for i := 0; i < len(elems); i++ {
n += len(elems[i])
}
var b Builder
b.Grow(n)
b.WriteString(elems[0])
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(s)
}
return b.String()
}
唯一不同在于在join方法內(nèi)調(diào)用了b.Grow(n)方法,這個是進(jìn)行初步的容量分配,而前面計(jì)算的n的長度就是我們要拼接的slice的長度,因?yàn)槲覀儌魅肭衅L度固定,所以提前進(jìn)行容量分配可以減少內(nèi)存分配,很高效。
切片append
因?yàn)?code>string類型底層也是byte類型數(shù)組,所以我們可以重新聲明一個切片,使用append進(jìn)行字符串拼接,使用方式如下:
buf := make([]byte, 0) base = "asong" buf = append(buf, base...) string(base)
如果想減少內(nèi)存分配,在將[]byte轉(zhuǎn)換為string類型時(shí)可以考慮使用強(qiáng)制轉(zhuǎn)換。
Benchmark對比
上面我們總共提供了6種方法,原理我們基本知道了,那么我們就使用Go語言中的Benchmark來分析一下到底哪種字符串拼接方式更高效。我們主要分兩種情況進(jìn)行分析:
- 少量字符串拼接
- 大量字符串拼接
因?yàn)榇a量有點(diǎn)多,下面只貼出分析結(jié)果,詳細(xì)代碼已經(jīng)上傳github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/string_join
我們先定義一個基礎(chǔ)字符串:
var base = "123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASFGHJKLZXCVBNM"
少量字符串拼接的測試我們就采用拼接一次的方式驗(yàn)證,base拼接base,因此得出benckmark結(jié)果:
goos: darwin goarch: amd64 pkg: asong.cloud/Golang_Dream/code_demo/string_join/once cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz BenchmarkSumString-16 21338802 49.19 ns/op 128 B/op 1 allocs/op BenchmarkSprintfString-16 7887808 140.5 ns/op 160 B/op 3 allocs/op BenchmarkBuilderString-16 27084855 41.39 ns/op 128 B/op 1 allocs/op BenchmarkBytesBuffString-16 9546277 126.0 ns/op 384 B/op 3 allocs/op BenchmarkJoinstring-16 24617538 48.21 ns/op 128 B/op 1 allocs/op BenchmarkByteSliceString-16 10347416 112.7 ns/op 320 B/op 3 allocs/op PASS ok asong.cloud/Golang_Dream/code_demo/string_join/once 8.412s
大量字符串拼接的測試我們先構(gòu)建一個長度為200的字符串切片:
var baseSlice []string
for i := 0; i < 200; i++ {
baseSlice = append(baseSlice, base)
}
然后遍歷這個切片不斷的進(jìn)行拼接,因?yàn)榭梢缘贸?code>benchmark:
goos: darwin goarch: amd64 pkg: asong.cloud/Golang_Dream/code_demo/string_join/muliti cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz BenchmarkSumString-16 7396 163612 ns/op 1277713 B/op 199 allocs/op BenchmarkSprintfString-16 5946 202230 ns/op 1288552 B/op 600 allocs/op BenchmarkBuilderString-16 262525 4638 ns/op 40960 B/op 1 allocs/op BenchmarkBytesBufferString-16 183492 6568 ns/op 44736 B/op 9 allocs/op BenchmarkJoinstring-16 398923 3035 ns/op 12288 B/op 1 allocs/op BenchmarkByteSliceString-16 144554 8205 ns/op 60736 B/op 15 allocs/op PASS ok asong.cloud/Golang_Dream/code_demo/string_join/muliti 10.699s
結(jié)論
通過兩次benchmark對比,我們可以看到當(dāng)進(jìn)行少量字符串拼接時(shí),直接使用+操作符進(jìn)行拼接字符串,效率還是挺高的,但是當(dāng)要拼接的字符串?dāng)?shù)量上來時(shí),+操作符的性能就比較低了;函數(shù)fmt.Sprintf還是不適合進(jìn)行字符串拼接,無論拼接字符串?dāng)?shù)量多少,性能損耗都很大,還是老老實(shí)實(shí)做他的字符串格式化就好了;strings.Builder無論是少量字符串的拼接還是大量的字符串拼接,性能一直都能穩(wěn)定,這也是為什么Go語言官方推薦使用strings.builder進(jìn)行字符串拼接的原因,在使用strings.builder時(shí)最好使用Grow方法進(jìn)行初步的容量分配,觀察strings.join方法的benchmark就可以發(fā)現(xiàn),因?yàn)槭褂昧?code>grow方法,提前分配好內(nèi)存,在字符串拼接的過程中,不需要進(jìn)行字符串的拷貝,也不需要分配新的內(nèi)存,這樣使用strings.builder性能最好,且內(nèi)存消耗最小。bytes.Buffer方法性能是低于strings.builder的,bytes.Buffer 轉(zhuǎn)化為字符串時(shí)重新申請了一塊空間,存放生成的字符串變量,不像strings.buidler這樣直接將底層的 []byte 轉(zhuǎn)換成了字符串類型返回,這就占用了更多的空間。
同步最后分析的結(jié)論:
無論什么情況下使用strings.builder進(jìn)行字符串拼接都是最高效的,不過要主要使用方法,記得調(diào)用grow進(jìn)行容量分配,才會高效。strings.join的性能約等于strings.builder,在已經(jīng)字符串slice的時(shí)候可以使用,未知時(shí)不建議使用,構(gòu)造切片也是有性能損耗的;如果進(jìn)行少量的字符串拼接時(shí),直接使用+操作符是最方便也是性能最高的,可以放棄strings.builder的使用。
綜合對比性能排序:
strings.join ≈ strings.builder > bytes.buffer > []byte轉(zhuǎn)換string > "+" > fmt.sprintf
總結(jié)
本文我們針對6種字符串的拼接方式進(jìn)行介紹,并通過benckmark對比了效率,無論什么時(shí)候使用strings.builder都不會錯,但是在少量字符串拼接時(shí),直接+也就是更優(yōu)的方式,具體業(yè)務(wù)場景具體分析,不要一概而論。
文中代碼已上傳github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/string_join
到此這篇關(guān)于Go語言如何高效的進(jìn)行字符串拼接(6種方式對比分析)的文章就介紹到這了,更多相關(guān)Go 字符串拼接內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Go語言創(chuàng)建WebSocket服務(wù)的實(shí)現(xiàn)示例
這篇文章主要介紹了使用Go語言創(chuàng)建WebSocket服務(wù)的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
Go中Gzip與json搭配實(shí)現(xiàn)數(shù)據(jù)壓縮demo
這篇文章主要為大家介紹了Go中Gzip與json搭配使用壓縮數(shù)據(jù)的實(shí)現(xiàn)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
詳解Golang中Context的三個常見應(yīng)用場景
Golang?context主要用于定義超時(shí)取消,取消后續(xù)操作,在不同操作中傳遞值。本文通過簡單易懂的示例進(jìn)行說明,感興趣的可以了解一下2022-12-12
golang語言編碼規(guī)范的實(shí)現(xiàn)
這篇文章主要介紹了golang語言編碼規(guī)范的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
一文帶大家了解Go語言中的內(nèi)聯(lián)優(yōu)化
內(nèi)聯(lián)優(yōu)化是一種常見的編譯器優(yōu)化策略,通俗來講,就是把函數(shù)在它被調(diào)用的地方展開,這樣可以減少函數(shù)調(diào)用所帶來的開銷,本文主要為大家介紹了Go中內(nèi)聯(lián)優(yōu)化的具體使用,需要的可以參考下2023-05-05
文字解說Golang Goroutine和線程的區(qū)別
goroutine 是 Go語言中的輕量級線程實(shí)現(xiàn),由 Go 運(yùn)行時(shí)(runtime)管理,使用每一個 go 關(guān)鍵字將會額外開啟一個新的協(xié)程 goroutine,今天通過本文給大家介紹下Golang Goroutine和線程的區(qū)別,感興趣的朋友一起看看吧2022-03-03

