您好,登錄后才能下訂單哦!
本篇內(nèi)容主要講解“Go語(yǔ)言怎么高效的進(jìn)行字符串拼接”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“Go語(yǔ)言怎么高效的進(jìn)行字符串拼接”吧!
我們首先來(lái)了解一下Go
語(yǔ)言中string
類(lèi)型的結(jié)構(gòu)定義,先來(lái)看一下官方定義:
// 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
是一個(gè)8
位字節(jié)的集合,通常但不一定代表UTF-8編碼的文本。string可以為空,但是不能為nil。string的值是不能改變的。
string
類(lèi)型本質(zhì)也是一個(gè)結(jié)構(gòu)體,定義如下:
type stringStruct struct { str unsafe.Pointer len int }
stringStruct
和slice
還是很相似的,str
指針指向的是某個(gè)數(shù)組的首地址,len
代表的就是數(shù)組長(zhǎng)度。怎么和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)⑹且粋€(gè)byte
類(lèi)型的指針,從這我們可以看出string
類(lèi)型底層是一個(gè)byte
類(lèi)型的數(shù)組,所以我們可以畫(huà)出這樣一個(gè)圖片:
string
類(lèi)型本質(zhì)上就是一個(gè)byte
類(lèi)型的數(shù)組,在Go
語(yǔ)言中string
類(lèi)型被設(shè)計(jì)為不可變的,不僅是在Go
語(yǔ)言,其他語(yǔ)言中string
類(lèi)型也是被設(shè)計(jì)為不可變的,這樣的好處就是:在并發(fā)場(chǎng)景下,我們可以在不加鎖的控制下,多次使用同一字符串,在保證高效共享的情況下而不用擔(dān)心安全問(wèn)題。
string
類(lèi)型雖然是不能更改的,但是可以被替換,因?yàn)?code>stringStruct中的str
指針是可以改變的,只是指針指向的內(nèi)容是不可以改變的,也就說(shuō)每一個(gè)更改字符串,就需要重新分配一次內(nèi)存,之前分配的空間會(huì)被gc
回收。
Go
語(yǔ)言原生支持使用+
操作符直接對(duì)兩個(gè)字符串進(jìn)行拼接,使用例子如下:
var s string s += "asong" s += "真帥"
這種方式使用起來(lái)最簡(jiǎn)單,基本所有語(yǔ)言都有提供這種方式,使用+
操作符進(jìn)行拼接時(shí),會(huì)對(duì)字符串進(jìn)行遍歷,計(jì)算并開(kāi)辟一個(gè)新的空間來(lái)存儲(chǔ)原來(lái)的兩個(gè)字符串。
fmt.Sprintf
Go
語(yǔ)言中默認(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ì)分析了,看到反射,就會(huì)產(chǎn)生性能的損耗,你們懂得?。?!
Go
語(yǔ)言提供了一個(gè)專(zhuān)門(mén)操作字符串的庫(kù)strings
,使用strings.Builder
可以進(jìn)行字符串拼接,提供了writeString
方法拼接字符串,使用方式如下:
var builder strings.Builder builder.WriteString("asong") builder.String()
strings.builder
的實(shí)現(xiàn)原理很簡(jiǎn)單,結(jié)構(gòu)如下:
type Builder struct { addr *Builder // of receiver, to detect copies by value buf []byte // 1 }
addr
字段主要是做copycheck
,buf
字段是一個(gè)byte
類(lèi)型的切片,這個(gè)就是用來(lái)存放字符串內(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
類(lèi)型,這里為了避免內(nèi)存拷貝的問(wèn)題,使用了強(qiáng)制轉(zhuǎn)換來(lái)避免內(nèi)存拷貝:
func (b *Builder) String() string { return *(*string)(unsafe.Pointer(&b.buf)) }
因?yàn)?code>string類(lèi)型底層就是一個(gè)byte
數(shù)組,所以我們就可以Go
語(yǔ)言的bytes.Buffer
進(jìn)行字符串拼接。bytes.Buffer
是一個(gè)一個(gè)緩沖byte
類(lèi)型的緩沖器,這個(gè)緩沖器里存放著都是byte
。使用方式如下:
buf := new(bytes.Buffer) buf.WriteString("asong") buf.String()
bytes.buffer
底層也是一個(gè)[]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
尾部寫(xiě)入數(shù)據(jù),從Buffer
頭部讀取數(shù)據(jù),所以off
字段用來(lái)記錄讀取位置,再利用切片的cap
特性來(lái)知道寫(xiě)入位置,這個(gè)不是本次的重點(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í)并不會(huì)申請(qǐng)內(nèi)存塊,只有在往里寫(xiě)數(shù)據(jù)時(shí)才會(huì)申請(qǐng),首次申請(qǐng)的大小即為寫(xiě)入數(shù)據(jù)的大小。如果寫(xiě)入的數(shù)據(jù)小于64字節(jié),則按64字節(jié)申請(qǐng)。采用動(dòng)態(tài)擴(kuò)展slice
的機(jī)制,字符串追加采用copy
的方式將追加的部分拷貝到尾部,copy
是內(nèi)置的拷貝函數(shù),可以減少內(nèi)存分配。
但是在將[]byte
轉(zhuǎn)換為string
類(lèi)型依舊使用了標(biāo)準(zhǔn)類(lèi)型,所以會(huì)發(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
方法可以將一個(gè)string
類(lèi)型的切片拼接成一個(gè)字符串,可以定義連接操作符,使用如下:
baseSlice := []string{"asong", "真帥"} strings.Join(baseSlice, "")
strings.join
也是基于strings.builder
來(lái)實(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)
方法,這個(gè)是進(jìn)行初步的容量分配,而前面計(jì)算的n的長(zhǎng)度就是我們要拼接的slice的長(zhǎng)度,因?yàn)槲覀儌魅肭衅L(zhǎng)度固定,所以提前進(jìn)行容量分配可以減少內(nèi)存分配,很高效。
append
因?yàn)?code>string類(lèi)型底層也是byte
類(lèi)型數(shù)組,所以我們可以重新聲明一個(gè)切片,使用append
進(jìn)行字符串拼接,使用方式如下:
buf := make([]byte, 0) base = "asong" buf = append(buf, base...) string(base)
如果想減少內(nèi)存分配,在將[]byte
轉(zhuǎn)換為string
類(lèi)型時(shí)可以考慮使用強(qiáng)制轉(zhuǎn)換。
上面我們總共提供了6種方法,原理我們基本知道了,那么我們就使用Go
語(yǔ)言中的Benchmark
來(lái)分析一下到底哪種字符串拼接方式更高效。我們主要分兩種情況進(jìn)行分析:
少量字符串拼接
大量字符串拼接
我們先定義一個(gè)基礎(chǔ)字符串:
var base = "123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASFGHJKLZXCVBNM"
少量字符串拼接的測(cè)試我們就采用拼接一次的方式驗(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
大量字符串拼接的測(cè)試我們先構(gòu)建一個(gè)長(zhǎng)度為200的字符串切片:
var baseSlice []string for i := 0; i < 200; i++ { baseSlice = append(baseSlice, base) }
然后遍歷這個(gè)切片不斷的進(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
通過(guò)兩次benchmark
對(duì)比,我們可以看到當(dāng)進(jìn)行少量字符串拼接時(shí),直接使用+
操作符進(jìn)行拼接字符串,效率還是挺高的,但是當(dāng)要拼接的字符串?dāng)?shù)量上來(lái)時(shí),+
操作符的性能就比較低了;函數(shù)fmt.Sprintf
還是不適合進(jìn)行字符串拼接,無(wú)論拼接字符串?dāng)?shù)量多少,性能損耗都很大,還是老老實(shí)實(shí)做他的字符串格式化就好了;strings.Builder
無(wú)論是少量字符串的拼接還是大量的字符串拼接,性能一直都能穩(wěn)定,這也是為什么Go
語(yǔ)言官方推薦使用strings.builder
進(jìn)行字符串拼接的原因,在使用strings.builder
時(shí)最好使用Grow
方法進(jìn)行初步的容量分配,觀(guān)察strings.join
方法的benchmark就可以發(fā)現(xiàn),因?yàn)槭褂昧?code>grow方法,提前分配好內(nèi)存,在字符串拼接的過(guò)程中,不需要進(jìn)行字符串的拷貝,也不需要分配新的內(nèi)存,這樣使用strings.builder
性能最好,且內(nèi)存消耗最小。bytes.Buffer
方法性能是低于strings.builder
的,bytes.Buffer
轉(zhuǎn)化為字符串時(shí)重新申請(qǐng)了一塊空間,存放生成的字符串變量,不像strings.buidler
這樣直接將底層的 []byte
轉(zhuǎn)換成了字符串類(lèi)型返回,這就占用了更多的空間。
同步最后分析的結(jié)論:
無(wú)論什么情況下使用strings.builder
進(jìn)行字符串拼接都是最高效的,不過(guò)要主要使用方法,記得調(diào)用grow
進(jìn)行容量分配,才會(huì)高效。strings.join
的性能約等于strings.builder
,在已經(jīng)字符串slice的時(shí)候可以使用,未知時(shí)不建議使用,構(gòu)造切片也是有性能損耗的;如果進(jìn)行少量的字符串拼接時(shí),直接使用+
操作符是最方便也是性能最高的,可以放棄strings.builder
的使用。
綜合對(duì)比性能排序:
strings.join
≈ strings.builder
> bytes.buffer
> []byte
轉(zhuǎn)換string
> "+" > fmt.sprintf
到此,相信大家對(duì)“Go語(yǔ)言怎么高效的進(jìn)行字符串拼接”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢(xún),關(guān)注我們,繼續(xù)學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。