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類型,需要將其轉(zhuǎn)換為字符串類型。當(dāng)數(shù)據(jù)量小時(shí),類型間轉(zhuǎn)換的開銷可以忽略不計(jì),但當(dāng)數(shù)據(jù)量增大后,可能成為性能瓶頸,使用高效的轉(zhuǎn)換方法能減少這方面的開銷
數(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字段,因此不能對string遍歷使用內(nèi)置的cap()函數(shù)那為什么string不需要cap字段呢?因?yàn)間o中string被設(shè)計(jì)為不可變類型(當(dāng)然在很多其他語言中也是),由于其不可像slice一樣追加元素,也就不需要cap字段判斷是否超出底層數(shù)組的容量,來決定是否擴(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ì)算出的哈希值也會發(fā)生變化,這樣再從map中定位時(shí)就找不到之前的value,因此其不可變特性能避免這種情況發(fā)生,string也適合作為map的鍵。除此之外,不可變特性也能保障數(shù)據(jù)的線程安全
常規(guī)實(shí)現(xiàn)
字符串不可變有很多好處,為了維持其不可變特性,字符串和字節(jié)數(shù)組互轉(zhuǎn)一般是通過數(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)簡單,但是通過底層數(shù)據(jù)復(fù)制實(shí)現(xiàn)的,在編譯期間分別轉(zhuǎn)換成對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 {
// 申請內(nèi)存
b = rawbyteslice(len(s))
}
// 復(fù)制數(shù)據(jù)
copy(b, s)
return b
}
其根據(jù)返回值是否逃逸到堆上,以及buf的長度是否足夠,判斷選擇使用buf還是調(diào)用rawbyteslice申請一個(gè)slice。但不管是哪種,都會執(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
// 賦值長度
stringStructOf(&str).len = len(b)
// 拷貝數(shù)據(jù)
memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
return
}
首先處理長度為0或1的情況,再判斷使用buf還是通過mallocgc新申請一段內(nèi)存,但無論哪種方式,最后都要拷貝數(shù)據(jù)
這里設(shè)置了轉(zhuǎn)換后字符串的len屬性
高效實(shí)現(xiàn)
如果程序保證不對底層數(shù)據(jù)進(jìn)行修改,那么只轉(zhuǎn)換類型,不拷貝數(shù)據(jù),是否可以提高性能?
unsafe.Pointer,int,uintpt這三種類型占用的內(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)上來看string可以看做[2]uintptr,[]byte切片類型可以看做 [3]uintptr

那么從string轉(zhuǎn)[]byte只需構(gòu)建出 [3]uintptr{ptr,len,len}
這里我們?yōu)閟lice結(jié)構(gòu)生成了cap字段,其實(shí)這里不生成cap字段對讀取操作沒有影響,但如果要往轉(zhuǎn)換后的slice append元素可能有問題,原因如下:
這樣做slice的cap屬性是隨機(jī)的,可能是大于len的值,那么append時(shí)就不會新開辟一段內(nèi)存存放元素,而是在原數(shù)組后面追加,如果后面的內(nèi)存不可寫就會panic
[]byte轉(zhuǎn)string更簡單,直接轉(zhuǎn)換指針類型即可,忽略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來轉(zhuǎn)換不同類型的指針,沒有底層數(shù)據(jù)的拷貝
性能測試
接下來對高效實(shí)現(xiàn)進(jìn)行性能測試,這里選用長度為100的字符串或字節(jié)數(shù)組進(jìn)行轉(zhuǎn)換
分別測試以下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)
}
測試結(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ù)組長度更長,性能提升更加明顯
總結(jié)
本文介紹了字符串和數(shù)組的底層數(shù)據(jù)結(jié)構(gòu),以及高效的互轉(zhuǎn)方法,需要注意的是,其適用于程序能保證不對底層數(shù)據(jù)進(jìn)行修改的場景。若不能保證,且底層數(shù)據(jù)被修改可能引發(fā)異常,則還是使用拷貝的方式
到此這篇關(guān)于Go中string與[]byte高效互轉(zhuǎn)的文章就介紹到這了,更多相關(guān)Go中string與[]byte互轉(zhuǎn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
GoLand 中設(shè)置默認(rèn)項(xiàng)目文件夾的實(shí)現(xiàn)
本文主要介紹了GoLand 中設(shè)置默認(rèn)項(xiàng)目文件夾的實(shí)現(xiàn),默認(rèn)項(xiàng)目文件夾會在你打開或新建項(xiàng)目時(shí)自動預(yù)選,避免每次都需要手動導(dǎo)航到目標(biāo)目錄,感興趣的可以了解一下2025-03-03
Go基于struct?tag實(shí)現(xiàn)結(jié)構(gòu)體字段級別的訪問控制
本文將會基于這個(gè)主題展開,討論Go中的結(jié)構(gòu)體tag究竟是什么,我們該如何利用它,另外,文末還提供了一個(gè)實(shí)際案例,實(shí)現(xiàn)結(jié)構(gòu)體字段級別的訪問,幫助我們進(jìn)一步提升對struct tag的理解2024-02-02
GoLang?socket網(wǎng)絡(luò)編程傳輸數(shù)據(jù)包時(shí)進(jìn)行長度校驗(yàn)的方法
在GoLang?socket網(wǎng)絡(luò)編程中,為了確保數(shù)據(jù)交互的穩(wěn)定性和安全性,通常會通過傳輸數(shù)據(jù)的長度進(jìn)行校驗(yàn),發(fā)送端首先發(fā)送數(shù)據(jù)長度,然后發(fā)送數(shù)據(jù)本體,接收端則根據(jù)接收到的數(shù)據(jù)長度和數(shù)據(jù)本體進(jìn)行比較,以此來確認(rèn)數(shù)據(jù)是否傳輸成功2024-11-11
Go語言使用Timeout Context取消任務(wù)的實(shí)現(xiàn)
本文主要介紹了Go語言使用Timeout Context取消任務(wù)的實(shí)現(xiàn),包括基本的任務(wù)取消和控制HTTP客戶端請求的超時(shí),具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01

