Go中string與[]byte高效互轉的方法實例
前言
當我們使用go進行數據序列化或反序列化操作時,可能經常涉及到字符串和字節數組的轉換。例如:
if str, err := json.Marshal(from); err != nil { panic(err) } else { return string(str) }
json序列化後為[]byte類型,需要將其轉換為字符串類型。當數據量小時,類型間轉換的開銷可以忽略不計,但當數據量增大後,可能成為性能瓶頸,使用高效的轉換方法能減少這方面的開銷
數據結構
在瞭解其如何轉換前,需要瞭解其底層數據結構
本文基於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的結構相比,string缺少一個表示容量的cap字段,因此不能對string遍歷使用內置的cap()函數那為什麼string不需要cap字段呢?因為go中string被設計為不可變類型(當然在很多其他語言中也是),由於其不可像slice一樣追加元素,也就不需要cap字段判斷是否超出底層數組的容量,來決定是否擴容
隻有len屬性不影響for-range等讀取操作,因為for-range操作隻根據len決定是否跳出循環
那為什麼字符串要設定為不可變呢?因為這樣能保證字符串的底層數組不發生改變
舉個例子,map中以string為鍵,如果底層字符數組改變,則計算出的哈希值也會發生變化,這樣再從map中定位時就找不到之前的value,因此其不可變特性能避免這種情況發生,string也適合作為map的鍵。除此之外,不可變特性也能保障數據的線程安全
常規實現
字符串不可變有很多好處,為瞭維持其不可變特性,字符串和字節數組互轉一般是通過數據拷貝的方式實現:
var a string = "hello world" var b []byte = []byte(a) // string轉[]byte a = string(b) // []byte轉string
這種方式實現簡單,但是通過底層數據復制實現的,在編譯期間分別轉換成對slicebytetostring和stringtoslicebyte的函數調用
string轉[]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 { // 申請內存 b = rawbyteslice(len(s)) } // 復制數據 copy(b, s) return b }
其根據返回值是否逃逸到堆上,以及buf的長度是否足夠,判斷選擇使用buf還是調用rawbyteslice申請一個slice。但不管是哪種,都會執行一次copy拷貝底層數據
[]byte轉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) // 拷貝數據 memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b))) return }
首先處理長度為0或1的情況,再判斷使用buf還是通過mallocgc新申請一段內存,但無論哪種方式,最後都要拷貝數據
這裡設置瞭轉換後字符串的len屬性
高效實現
如果程序保證不對底層數據進行修改,那麼隻轉換類型,不拷貝數據,是否可以提高性能?
unsafe.Pointer,int,uintpt這三種類型占用的內存大小相同
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
因此從底層結構上來看string可以看做[2]uintptr,[]byte切片類型可以看做 [3]uintptr
那麼從string轉[]byte隻需構建出 [3]uintptr{ptr,len,len}
這裡我們為slice結構生成瞭cap字段,其實這裡不生成cap字段對讀取操作沒有影響,但如果要往轉換後的slice append元素可能有問題,原因如下:
這樣做slice的cap屬性是隨機的,可能是大於len的值,那麼append時就不會新開辟一段內存存放元素,而是在原數組後面追加,如果後面的內存不可寫就會panic
[]byte轉string更簡單,直接轉換指針類型即可,忽略cap字段
實現如下:
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來轉換不同類型的指針,沒有底層數據的拷貝
性能測試
接下來對高效實現進行性能測試,這裡選用長度為100的字符串或字節數組進行轉換
分別測試以下4個方法:
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) }
測試結果如下:
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
可以看出性能差距比較大,如果需要轉換的字符串或字節數組長度更長,性能提升更加明顯
總結
本文介紹瞭字符串和數組的底層數據結構,以及高效的互轉方法,需要註意的是,其適用於程序能保證不對底層數據進行修改的場景。若不能保證,且底層數據被修改可能引發異常,則還是使用拷貝的方式
到此這篇關於Go中string與[]byte高效互轉的文章就介紹到這瞭,更多相關Go中string與[]byte互轉內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 關於Go 空結構體的 3 種使用場景
- 深入理解go slice結構
- go 迭代string數組操作 go for string[]
- Go結構體SliceHeader及StringHeader作用詳解
- Go語言模型:string的底層數據結構與高效操作詳解