[]byte與string的兩種轉換方式和底層實現
不過你發現沒fasthttp關于string和[]byte的轉換方式和大家平常普遍使用的方式不一樣,fasthttp轉換實現如下:
//[]byte轉string
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
//string轉[]byte
func s2b(s string) (b []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Cap = sh.Len
bh.Len = sh.Len
return b
}
為什么不用我們常見的string和[]byte的轉換方式呢?這樣做是怎么提高性能的呢?...
帶著這些疑問,今天將分享下并總結string和[]byte的轉換方式,不同的轉換方式之間的實現和區別!
圖片
兩種轉換方式
如果此時此刻你剛好遇到面試官問你string和[]byte如何進行轉換,有幾種方式?你能答上來嗎
反正在寫這篇文章之前小許估計是答不出來的,哈哈!
畢竟知道的越多,不知道的也越多嘛
那今天我們就來聊聊,繼續往下讀之前,我們先了解下這兩種數據類型:
string和[]byte
圖片
??上圖中可以看出 stringStruct和slice還是有一些相似之處,str和array指針指向底層數組的地址,len代表的就是數組長度。
關于string類型,在go標準庫中官方說明如下:
// 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位字節的集合,string的定義在上圖中左側,通常但不一定代表UTF-8編碼的文本。string可以為空,但是不能為nil,并且string的值是不能改變的。
??為什么string類型沒有cap字段
string的不可變性,也就不能直接向底層數組追加元素,所以不需要Cap。
而[]byte就是一個byte類型的切片,切片本質也是一個結構體。
?? 這里我們先記住下這兩種數據類型的特點,對后面的了解兩者的轉換有幫助!
標準方式
Golang中string與[]byte的互換,這是我們常用的,也是立馬能想到的轉換方式,這種方式稱為標準方式。
// string 轉 []byte
s1 := "xiaoxu"
b := []byte(s1)
// []byte 轉 string
s2 := string(b)
那還有其他方式嗎?當然有的,那就是強轉換
強轉換方式
強轉換方式是通過unsafe和reflect包來實現的,代碼如下:
//[]byte轉string
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
//string轉[]byte
func s2b(s string) (b []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Cap = sh.Len
bh.Len = sh.Len
return b
}
可以看出利用reflect.SliceHeader(代表一個運行時的切片) 和 unsafe.Pointer進行指針替換。
??為什么可以這么做呢?
前面我們在講string和[]byte類型的時候就提了,因為兩者的底層結構的字段相似!
array和str的len是一致的,而唯一不同的就是cap字段,所以他們的內存布局上是對齊的。
分析
我們看下這兩種轉換方式底層是如何實現的,這些實現代碼在標準庫中都是有的,下面底層實現的代碼來自Go 1.18.6版本。
標準方式底層實現
string轉[]byte底層實現
先看string轉[]byte的實現,(實現源碼在 src/runtime/string.go 中)
const tmpStringBufSize = 32
//長度32的數組
type tmpBuf [tmpStringBufSize]byte
//時間函數
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
//判斷字符串長度是否小于等于32
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
//預定義數組長度不夠,重新分配內存
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}
// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
//rawbyteslice函數 分配一個新的字節片。字節片未歸零
func rawbyteslice(size int) (b []byte) {
cap := roundupsize(uintptr(size))
p := mallocgc(cap, nil, false)
if cap != uintptr(size) {
memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
}
*(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
return
}
圖片
上面代碼可以看出string轉[]byte是,會根據字符串長度來決定是否需要重新分配一塊內存。
- ? 預先定義了一個長度為32的數組
- ? 若字符串的長度不超過這個長度32的數組,copy函數實現string到[]byte的拷貝
- ? 若字符串的長度超過了這個長度32的數組,重新分配一塊內存了,再進行copy
[]byte轉string底層實現
再看[]byte轉string的實現,(實現源碼在 src/runtime/string.go 中)
const tmpStringBufSize = 32
//長度32的數組
type tmpBuf [tmpStringBufSize]byte
//實現函數
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
...
if n == 1 {
p := unsafe.Pointer(&staticuint64s[*ptr])
if goarch.BigEndian {
p = add(p, 7)
}
stringStructOf(&str).str = p
stringStructOf(&str).len = 1
return
}
var p unsafe.Pointer
//判斷字符串長度是否小于等于32
if buf != nil && n <= len(buf) {
p = unsafe.Pointer(buf)
} else {
p = mallocgc(uintptr(n), nil, false)
}
stringStructOf(&str).str = p
stringStructOf(&str).len = n
//拷貝byte數組至字符串
memmove(p, unsafe.Pointer(ptr), uintptr(n))
return
}
跟string轉[]byte一樣,當數組長度超過32時,同樣需要調用mallocgc分配一塊新內存
強轉換底層實現
從標準的轉換方式中,我們知道如果字符串長度超過32的話,會重新分配一塊新內存,進行內存拷貝。
//string轉[]byte
func s2b(s string) (b []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Cap = sh.Len
bh.Len = sh.Len
return b
}
強轉換過程中,通過 神奇的unsafe.Pointer指針
? 任何類型的指針 *T 都可以轉換為unsafe.Pointer類型的指針,可以存儲任何變量的地址
? unsafe.Pointer 類型的指針也可以轉換回普通指針,并且可以和類型*T不相同
?? refletc包的 reflect.SliceHeader 和 reflect.StringHeader分別代表什么意思?
reflect.SliceHeader:slice類型的運行時表示形式
reflect.StringHeader:string類型的運行時表示形式
//slice在運行時的描述符
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
//string在運行時的描述符
type StringHeader struct {
Data uintptr
Len int
}
*(reflect.SliceHeader)(unsafe.Pointer(&b)) 的目的就是通過unsafe.Pointer 把它們轉換為 *reflect.SliceHeader 指針。
而運行時表現形式 SliceHeader 和 StringHeader,而這兩個結構體都有一個 Data 字段,用于存放指向真實內容的指針。
??[]byte 和 string之間的轉換,就可以理解為是通過 unsafe.Pointer 把 *SliceHeader 轉為 *StringHeader,也就是 *[]byte 和 *string之間的轉換。
那么我們就可以理解相對于標準轉換方式,強轉換方式的優點在哪了!
直接替換指針的指向,避免了申請新內存(零拷貝),因為兩者指向的底層字段Data地址相同
總結
今天小許和大家一起了解了[]byte和string類型,以及[]byte和string的兩種轉換方式。
不過Go語言提供給我們使用的還是標準轉換方式,主要是因為在你不確定安全隱患的情況下,使用強轉化方式可能不必要的問題。
不過像fasthttp那樣,對程序對運行性能有高要求,那就可以考慮使用強轉換方式!