你所知道的 string 和 []byte 轉換方法可能是錯的

前幾天閒聊的時候,景埕 [1] 說網上很多 string 和 []byte 的轉換都是有問題的,當時並沒有在意,轉過身沒幾天我偶然看到字節跳動的一篇文章,其中提到了他們是如何優化 string 和 []byte 轉換的,我便問景埕有沒有問題,討論過程中學到了很多,於是便有了這篇總結。

讓我們看看問題代碼,類似的 string 和 []byte 轉換代碼在網上非常常見:

func StringToSliceByte(s string) []byte {
 l := len(s)
 return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
  Data: (*(*reflect.StringHeader)(unsafe.Pointer(&s))).Data,
  Len:  l,
  Cap:  l,
 }))
}

大家之所以不願意直接通過 []byte(string) 把 string 轉換爲 []byte,是因爲那樣會牽扯內存拷貝,而通過 unsafe.Pointer[2] 來做類型轉換,沒有內存拷貝,從而達到提升性能的目的。

問題代碼到底有沒有問題?其實當我把代碼拷貝到 vscode 之後就有提示了:

SliceHeader is the runtime representation of a slice. It cannot be used safely or portably and its representation may change in a later release. Moreover, the Data field is not sufficient to guarantee the data it references will not be garbage collected, so programs must keep a separate, correctly typed pointer to the underlying data.

首先,reflect.SliceHeader[3] 作爲 slice 的運行時表示,以後可能會改變,直接使用它存在風險;其次,Data 字段無法保證它指向的數據不被 GC 垃圾回收。

前一個問題還好說,但是後面提的 GC 問題則是個大問題!爲什麼會存在 GC 問題,我們不妨看看 reflect.SliceHeader 和 reflect.StringHeader 的定義:

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}

type StringHeader struct {
 Data uintptr
 Len  int
}

如上所示,Data 的類型是 uintptr,雖然有一個 ptr 後綴,但是它本質上還是一個整型,並不是指針,也就是說,它並不會持有它指向的數據,所以數據可能會被 GC 回收。

知道了前因後果,那麼讓我們構造一段代碼來證明存在 GC 問題:

package main

import (
 "fmt"
 "reflect"
 "runtime"
 "unsafe"
)

func main() {
 fmt.Printf("%s\n", test())
}

func test() []byte {
 defer runtime.GC()
 x := make([]byte, 5)
 x[0] = 'h'
 x[1] = 'e'
 x[2] = 'l'
 x[3] = 'l'
 x[4] = 'o'
 return StringToSliceByte1(string(x))
}

func StringToSliceByte1(s string) []byte {
 l := len(s)
 return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
  Data: (*(*reflect.StringHeader)(unsafe.Pointer(&s))).Data,
  Len:  l,
  Cap:  l,
 }))
}

注:因爲靜態字符串存儲在 TEXT 區,不會被 GC 回收,所以使用了動態字符串。

當我們運行上面的代碼,並不會輸出 hello,而是會輸出亂碼,原因是對應的數據已經被 GC 回收了,如果我們去掉 runtime.GC() 再運行,那麼輸出大概率會恢復正常。

由此可見,因爲 Data 是 uintptr 類型,所以任何對它的賦值都是不安全的。原本問題到這裏就應該告一段落了,但是 unsafe.Pointer 文檔裏恰好就有一個直接對 Data 賦值的例子:Conversion of a reflect.SliceHeader or reflect.StringHeader Data field to or from Pointer.

var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n

到底是文檔有誤,還是我們的推斷錯了,繼續看文檔裏的說明:

the reflect data structures SliceHeader and StringHeader declare the field Data as a uintptr to keep callers from changing the result to an arbitrary type without first importing “unsafe”. However, this means that SliceHeader and StringHeader are only valid when interpreting the content of an actual slice or string value.

也就是說,只有當操作實際存在的 slice 或 string 的時候,SliceHeader 或 StringHeader 纔是有效的,回想最初的代碼,因爲操作 reflect.SliceHeader 的時候,並沒有實際存在的 slice,所以是不符合 unsafe.Pointer 使用規範的(golang-nuts[4]),按照要求調整一下:

func StringToSliceByte(s string) []byte {
 var b []byte
 l := len(s)
 p := (*reflect.SliceHeader)(unsafe.Pointer(&b))
 p.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
 p.Len = l
 p.Cap = l
 return b
}

再用測試代碼跑一下,結果發現輸出正常了。不過有人可能會問了,之前不是說了 uintptr 不是指針,不能阻止數據被 GC 回收,可是爲什麼 GC 沒有效果?實際上這是因爲編譯器對 *reflect.{Slice,String}Header 做了特殊處理 [5],具體細節不展開了。

如果你想驗證是否存在特殊處理,可以使用自定義的類型反向驗證一下:

type StringHeader struct {
 Data uintptr
 Len  int
}

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}

func StringToSliceByte(s string) []byte {
 var b []byte
 l := len(s)
 p := (*SliceHeader)(unsafe.Pointer(&b))
 p.Data = (*StringHeader)(unsafe.Pointer(&s)).Data
 p.Len = l
 p.Cap = l
 return b
}

你會發現,如果沒有使用 reflect 裏的類型,那麼輸出就又不正常了。從而反向驗證了編譯器確實對 *reflect.{Slice,String}Header 做了特殊處理。

現在,我們基本搞清楚了 string 和 []byte 轉換中的各種坑,下面看看如何寫出準確的轉換代碼,雖然編譯器在其中耍了一些小動作,但是我們不應該依賴這些黑科技。

既然 uintptr 不是指針,那麼我們改用 unsafe.Pointer,如此數據就不會被 GC 回收了:

type StringHeader struct {
 Data unsafe.Pointer
 Len  int
}

type SliceHeader struct {
 Data unsafe.Pointer
 Len  int
 Cap  int
}

func StringToSliceByte3(s string) []byte {
 var b []byte
 l := len(s)
 p := (*SliceHeader)(unsafe.Pointer(&b))
 p.Data = (*StringHeader)(unsafe.Pointer(&s)).Data
 p.Len = l
 p.Cap = l
 return b
}

不過更簡單的做法是徹底拋棄 reflect 包,具體參考 gin 中的 bytesconv[6]:

func StringToBytes(s string) []byte {
 return *(*[]byte)(unsafe.Pointer(
  &struct {
   string
   Cap int
  }{s, len(s)},
 ))
}

func BytesToString([]byte) string {
 return *(*string)(unsafe.Pointer(&b))
}

至此,我們完美解決了 string 和 []byte 的轉換問題。

參考資料

[1]

景埕: https://github.com/diogin

[2]

unsafe.Pointer: https://pkg.go.dev/unsafe#Pointer

[3]

reflect.SliceHeader: https://pkg.go.dev/reflect#SliceHeader

[4]

golang-nuts: https://groups.google.com/g/golang-nuts/c/Zsfk-VMd_fU/m/qJzdycRiCwAJ

[5]

特殊處理: https://github.com/golang/go/issues/19168

[6]

gin 中的 bytesconv: https://github.com/gin-gonic/gin/blob/master/internal/bytesconv/bytesconv.go

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/T--shUtArU-asFthtR7waA