<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
日常業務開發中離不開字串的拼接操作,不同語言的字串實現方式都不同,在
Go
語言中就提供了6種方式進行字串拼接,那這幾種拼接方式該如何選擇呢?使用那個更高效呢?本文我們就一起來分析一下。本文使用Go語言版本:1.17.1
我們首先來了解一下Go
語言中string
型別的結構定義,先來看一下官方定義:
// 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
位位元組的集合,通常但不一定代表UTF-8編碼的文字。string可以為空,但是不能為nil。string的值是不能改變的。
string
型別本質也是一個結構體,定義如下:
type stringStruct struct { str unsafe.Pointer len int }
stringStruct
和slice
還是很相似的,str
指標指向的是某個陣列的首地址,len
代表的就是陣列長度。怎麼和slice
這麼相似,底層指向的也是陣列,是什麼陣列呢?我們看看他在範例化時呼叫的方法:
//go:nosplit func gostringnocopy(str *byte) string { ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} s := *(*string)(unsafe.Pointer(&ss)) return s }
入參是一個byte
型別的指標,從這我們可以看出string
型別底層是一個byte
型別的陣列,所以我們可以畫出這樣一個圖片:
string
型別本質上就是一個byte
型別的陣列,在Go
語言中string
型別被設計為不可變的,不僅是在Go
語言,其他語言中string
型別也是被設計為不可變的,這樣的好處就是:在並行場景下,我們可以在不加鎖的控制下,多次使用同一字串,在保證高效共用的情況下而不用擔心安全問題。
string
型別雖然是不能更改的,但是可以被替換,因為stringStruct
中的str
指標是可以改變的,只是指標指向的內容是不可以改變的,也就說每一個更改字串,就需要重新分配一次記憶體,之前分配的空間會被gc
回收。
關於string
型別的知識點就描述這麼多,方便我們後面分析字串拼接。
Go
語言原生支援使用+
操作符直接對兩個字串進行拼接,使用例子如下:
var s string s += "asong" s += "真帥"
這種方式使用起來最簡單,基本所有語言都有提供這種方式,使用+
操作符進行拼接時,會對字串進行遍歷,計算並開闢一個新的空間來儲存原來的兩個字串。
fmt.Sprintf
Go
語言中預設使用函數fmt.Sprintf
進行字串格式化,所以也可使用這種方式進行字串拼接:
str := "asong" str = fmt.Sprintf("%s%s", str, str)
fmt.Sprintf
實現原理主要是使用到了反射,具體原始碼分析因為篇幅的原因就不在這裡詳細分析了,看到反射,就會產生效能的損耗,你們懂得!!!
Go
語言提供了一個專門操作字串的庫strings
,使用strings.Builder
可以進行字串拼接,提供了writeString
方法拼接字串,使用方式如下:
var builder strings.Builder builder.WriteString("asong") builder.String()
strings.builder
的實現原理很簡單,結構如下:
type Builder struct { addr *Builder // of receiver, to detect copies by value buf []byte // 1 }
addr
欄位主要是做copycheck
,buf
欄位是一個byte
型別的切片,這個就是用來存放字串內容的,提供的writeString()
方法就是像切片buf
中追加資料:
func (b *Builder) WriteString(s string) (int, error) { b.copyCheck() b.buf = append(b.buf, s...) return len(s), nil }
提供的String
方法就是將[]]byte
轉換為string
型別,這裡為了避免記憶體拷貝的問題,使用了強制轉換來避免記憶體拷貝:
func (b *Builder) String() string { return *(*string)(unsafe.Pointer(&b.buf)) }
因為string
型別底層就是一個byte
陣列,所以我們就可以Go
語言的bytes.Buffer
進行字串拼接。bytes.Buffer
是一個一個緩衝byte
型別的緩衝器,這個緩衝器裡存放著都是byte
。使用方式如下:
buf := new(bytes.Buffer) buf.WriteString("asong") buf.String()
bytes.buffer
底層也是一個[]byte
切片,結構體如下:
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. }
因為bytes.Buffer
可以持續向Buffer
尾部寫入資料,從Buffer
頭部讀取資料,所以off
欄位用來記錄讀取位置,再利用切片的cap
特性來知道寫入位置,這個不是本次的重點,重點看一下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 }
切片在建立時並不會申請記憶體塊,只有在往裡寫資料時才會申請,首次申請的大小即為寫入資料的大小。如果寫入的資料小於64位元組,則按64位元組申請。採用動態擴充套件slice
的機制,字串追加採用copy
的方式將追加的部分拷貝到尾部,copy
是內建的拷貝函數,可以減少記憶體分配。
但是在將[]byte
轉換為string
型別依舊使用了標準型別,所以會發生記憶體分配:
func (b *Buffer) String() string { if b == nil { // Special case, useful in debugging. return "<nil>" } return string(b.buf[b.off:]) }
Strings.join
方法可以將一個string
型別的切片拼接成一個字串,可以定義連線操作符,使用如下:
baseSlice := []string{"asong", "真帥"} strings.Join(baseSlice, "")
strings.join
也是基於strings.builder
來實現的,程式碼如下:
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
方法內呼叫了b.Grow(n)
方法,這個是進行初步的容量分配,而前面計算的n的長度就是我們要拼接的slice的長度,因為我們傳入切片長度固定,所以提前進行容量分配可以減少記憶體分配,很高效。
append
因為string
型別底層也是byte
型別陣列,所以我們可以重新宣告一個切片,使用append
進行字串拼接,使用方式如下:
buf := make([]byte, 0) base = "asong" buf = append(buf, base...) string(base)
如果想減少記憶體分配,在將[]byte
轉換為string
型別時可以考慮使用強制轉換。
上面我們總共提供了6種方法,原理我們基本知道了,那麼我們就使用Go
語言中的Benchmark
來分析一下到底哪種字串拼接方式更高效。我們主要分兩種情況進行分析:
因為程式碼量有點多,下面只貼出分析結果,詳細程式碼已經上傳github
:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/string_join
我們先定義一個基礎字串:
var base = "123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASFGHJKLZXCVBNM"
少量字串拼接的測試我們就採用拼接一次的方式驗證,base拼接base,因此得出benckmark結果:
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
大量字串拼接的測試我們先構建一個長度為200的字串切片:
var baseSlice []string for i := 0; i < 200; i++ { baseSlice = append(baseSlice, base) }
然後遍歷這個切片不斷的進行拼接,因為可以得出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
通過兩次benchmark
對比,我們可以看到當進行少量字串拼接時,直接使用+
操作符進行拼接字串,效率還是挺高的,但是當要拼接的字串數量上來時,+
操作符的效能就比較低了;函數fmt.Sprintf
還是不適合進行字串拼接,無論拼接字串數量多少,效能損耗都很大,還是老老實實做他的字串格式化就好了;strings.Builder
無論是少量字串的拼接還是大量的字串拼接,效能一直都能穩定,這也是為什麼Go
語言官方推薦使用strings.builder
進行字串拼接的原因,在使用strings.builder
時最好使用Grow
方法進行初步的容量分配,觀察strings.join
方法的benchmark就可以發現,因為使用了grow
方法,提前分配好記憶體,在字串拼接的過程中,不需要進行字串的拷貝,也不需要分配新的記憶體,這樣使用strings.builder
效能最好,且記憶體消耗最小。bytes.Buffer
方法效能是低於strings.builder
的,bytes.Buffer
轉化為字串時重新申請了一塊空間,存放生成的字串變數,不像strings.buidler
這樣直接將底層的 []byte
轉換成了字串型別返回,這就佔用了更多的空間。
同步最後分析的結論:
無論什麼情況下使用strings.builder
進行字串拼接都是最高效的,不過要主要使用方法,記得呼叫grow
進行容量分配,才會高效。strings.join
的效能約等於strings.builder
,在已經字串slice的時候可以使用,未知時不建議使用,構造切片也是有效能損耗的;如果進行少量的字串拼接時,直接使用+
操作符是最方便也是效能最高的,可以放棄strings.builder
的使用。
綜合對比效能排序:
strings.join
≈ strings.builder
> bytes.buffer
> []byte
轉換string
> "+" > fmt.sprintf
本文我們針對6
種字串的拼接方式進行介紹,並通過benckmark
對比了效率,無論什麼時候使用strings.builder
都不會錯,但是在少量字串拼接時,直接+
也就是更優的方式,具體業務場景具體分析,不要一概而論。
文中程式碼已上傳github
:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/string_join
到此這篇關於Go語言如何高效的進行字串拼接(6種方式對比分析)的文章就介紹到這了,更多相關Go 字串拼接內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45