Go中string與[]byte高效互轉(zhuǎn)的方法實(shí)例
前言
當(dāng)我們使用go進(jìn)行數(shù)據(jù)序列化或反序列化操作時(shí),可能經(jīng)常涉及到字符串和字節(jié)數(shù)組的轉(zhuǎn)換。例如:
if str, err := json.Marshal(from); err != nil { panic(err) } else { return string(str) }
json序列化后為[]byte類(lèi)型,需要將其轉(zhuǎn)換為字符串類(lèi)型。當(dāng)數(shù)據(jù)量小時(shí),類(lèi)型間轉(zhuǎn)換的開(kāi)銷(xiāo)可以忽略不計(jì),但當(dāng)數(shù)據(jù)量增大后,可能成為性能瓶頸,使用高效的轉(zhuǎn)換方法能減少這方面的開(kāi)銷(xiāo)
數(shù)據(jù)結(jié)構(gòu)
在了解其如何轉(zhuǎn)換前,需要了解其底層數(shù)據(jù)結(jié)構(gòu)
本文基于go 1.13.12
string:
type stringStruct struct { str unsafe.Pointer len int }
slice:
type slice struct { array unsafe.Pointer len int cap int }
與slice的結(jié)構(gòu)相比,string缺少一個(gè)表示容量的cap字段,因此不能對(duì)string遍歷使用內(nèi)置的cap()函數(shù)那為什么string不需要cap字段呢?因?yàn)間o中string被設(shè)計(jì)為不可變類(lèi)型(當(dāng)然在很多其他語(yǔ)言中也是),由于其不可像slice一樣追加元素,也就不需要cap字段判斷是否超出底層數(shù)組的容量,來(lái)決定是否擴(kuò)容
只有l(wèi)en屬性不影響for-range等讀取操作,因?yàn)閒or-range操作只根據(jù)len決定是否跳出循環(huán)
那為什么字符串要設(shè)定為不可變呢?因?yàn)檫@樣能保證字符串的底層數(shù)組不發(fā)生改變
舉個(gè)例子,map中以string為鍵,如果底層字符數(shù)組改變,則計(jì)算出的哈希值也會(huì)發(fā)生變化,這樣再?gòu)膍ap中定位時(shí)就找不到之前的value,因此其不可變特性能避免這種情況發(fā)生,string也適合作為map的鍵。除此之外,不可變特性也能保障數(shù)據(jù)的線(xiàn)程安全
常規(guī)實(shí)現(xiàn)
字符串不可變有很多好處,為了維持其不可變特性,字符串和字節(jié)數(shù)組互轉(zhuǎn)一般是通過(guò)數(shù)據(jù)拷貝的方式實(shí)現(xiàn):
var a string = "hello world" var b []byte = []byte(a) // string轉(zhuǎn)[]byte a = string(b) // []byte轉(zhuǎn)string
這種方式實(shí)現(xiàn)簡(jiǎn)單,但是通過(guò)底層數(shù)據(jù)復(fù)制實(shí)現(xiàn)的,在編譯期間分別轉(zhuǎn)換成對(duì)slicebytetostring和stringtoslicebyte的函數(shù)調(diào)用
string轉(zhuǎn)[]byte
func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { *buf = tmpBuf{} b = buf[:len(s)] } else { // 申請(qǐng)內(nèi)存 b = rawbyteslice(len(s)) } // 復(fù)制數(shù)據(jù) copy(b, s) return b }
其根據(jù)返回值是否逃逸到堆上,以及buf的長(zhǎng)度是否足夠,判斷選擇使用buf還是調(diào)用rawbyteslice申請(qǐng)一個(gè)slice。但不管是哪種,都會(huì)執(zhí)行一次copy拷貝底層數(shù)據(jù)
[]byte轉(zhuǎn)string
func slicebytetostring(buf *tmpBuf, b []byte) (str string) { l := len(b) if l == 0 { return "" } if l == 1 { stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]]) stringStructOf(&str).len = 1 return } var p unsafe.Pointer if buf != nil && len(b) <= len(buf) { p = unsafe.Pointer(buf) } else { p = mallocgc(uintptr(len(b)), nil, false) } // 賦值底層指針 stringStructOf(&str).str = p // 賦值長(zhǎng)度 stringStructOf(&str).len = len(b) // 拷貝數(shù)據(jù) memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b))) return }
首先處理長(zhǎng)度為0或1的情況,再判斷使用buf還是通過(guò)mallocgc新申請(qǐng)一段內(nèi)存,但無(wú)論哪種方式,最后都要拷貝數(shù)據(jù)
這里設(shè)置了轉(zhuǎn)換后字符串的len屬性
高效實(shí)現(xiàn)
如果程序保證不對(duì)底層數(shù)據(jù)進(jìn)行修改,那么只轉(zhuǎn)換類(lèi)型,不拷貝數(shù)據(jù),是否可以提高性能?
unsafe.Pointer,int,uintpt這三種類(lèi)型占用的內(nèi)存大小相同
var v1 unsafe.Pointer var v2 int var v3 uintptr fmt.Println(unsafe.Sizeof(v1)) // 8 fmt.Println(unsafe.Sizeof(v2)) // 8 fmt.Println(unsafe.Sizeof(v3)) // 8
因此從底層結(jié)構(gòu)上來(lái)看string可以看做[2]uintptr,[]byte切片類(lèi)型可以看做 [3]uintptr
那么從string轉(zhuǎn)[]byte只需構(gòu)建出 [3]uintptr{ptr,len,len}
這里我們?yōu)閟lice結(jié)構(gòu)生成了cap字段,其實(shí)這里不生成cap字段對(duì)讀取操作沒(méi)有影響,但如果要往轉(zhuǎn)換后的slice append元素可能有問(wèn)題,原因如下:
這樣做slice的cap屬性是隨機(jī)的,可能是大于len的值,那么append時(shí)就不會(huì)新開(kāi)辟一段內(nèi)存存放元素,而是在原數(shù)組后面追加,如果后面的內(nèi)存不可寫(xiě)就會(huì)panic
[]byte轉(zhuǎn)string更簡(jiǎn)單,直接轉(zhuǎn)換指針類(lèi)型即可,忽略cap字段
實(shí)現(xiàn)如下:
func stringTobyteSlice(s string) []byte { tmp1 := (*[2]uintptr)(unsafe.Pointer(&s)) tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]} return *(*[]byte)(unsafe.Pointer(&tmp2)) } func byteSliceToString(bytes []byte) string { return *(*string)(unsafe.Pointer(&bytes)) }
這里使用unsafe.Pointer來(lái)轉(zhuǎn)換不同類(lèi)型的指針,沒(méi)有底層數(shù)據(jù)的拷貝
性能測(cè)試
接下來(lái)對(duì)高效實(shí)現(xiàn)進(jìn)行性能測(cè)試,這里選用長(zhǎng)度為100的字符串或字節(jié)數(shù)組進(jìn)行轉(zhuǎn)換
分別測(cè)試以下4個(gè)方法:
func stringTobyteSlice(s string) []byte { tmp1 := (*[2]uintptr)(unsafe.Pointer(&s)) tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]} return *(*[]byte)(unsafe.Pointer(&tmp2)) } func stringTobyteSliceOld(s string) []byte { return []byte(s) } func byteSliceToString(bytes []byte) string { return *(*string)(unsafe.Pointer(&bytes)) } func byteSliceToStringOld(bytes []byte) string { return string(bytes) }
測(cè)試結(jié)果如下:
BenchmarkStringToByteSliceOld-12 28637332 42.0 ns/op
BenchmarkStringToByteSliceNew-12 1000000000 0.496 ns/op
BenchmarkByteSliceToStringOld-12 32595271 36.0 ns/op
BenchmarkByteSliceToStringNew-12 1000000000 0.256 ns/op
可以看出性能差距比較大,如果需要轉(zhuǎn)換的字符串或字節(jié)數(shù)組長(zhǎng)度更長(zhǎng),性能提升更加明顯
總結(jié)
本文介紹了字符串和數(shù)組的底層數(shù)據(jù)結(jié)構(gòu),以及高效的互轉(zhuǎn)方法,需要注意的是,其適用于程序能保證不對(duì)底層數(shù)據(jù)進(jìn)行修改的場(chǎng)景。若不能保證,且底層數(shù)據(jù)被修改可能引發(fā)異常,則還是使用拷貝的方式
到此這篇關(guān)于Go中string與[]byte高效互轉(zhuǎn)的文章就介紹到這了,更多相關(guān)Go中string與[]byte互轉(zhuǎn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語(yǔ)言同步與異步執(zhí)行多個(gè)任務(wù)封裝詳解(Runner和RunnerAsync)
這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言同步與異步執(zhí)行多個(gè)任務(wù)封裝(Runner和RunnerAsync)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01Golang實(shí)現(xiàn)http server提供壓縮文件下載功能
這篇文章主要介紹了Golang實(shí)現(xiàn)http server提供壓縮文件下載功能,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Go?iota關(guān)鍵字與枚舉類(lèi)型實(shí)現(xiàn)原理
這篇文章主要介紹了Go?iota關(guān)鍵字與枚舉類(lèi)型實(shí)現(xiàn)原理,iota是go語(yǔ)言的常量計(jì)數(shù)器,只能在常量的表達(dá)式中使用,更多相關(guān)內(nèi)容需要的小伙伴可以參考一下2022-07-07GoLang中panic與recover函數(shù)以及defer語(yǔ)句超詳細(xì)講解
這篇文章主要介紹了GoLang的panic、recover函數(shù),以及defer語(yǔ)句,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-01-01Golang?手寫(xiě)一個(gè)簡(jiǎn)單的并發(fā)任務(wù)?manager
這篇文章主要介紹了Golang?手寫(xiě)一個(gè)簡(jiǎn)單的并發(fā)任務(wù)?manager,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08