區別
在我們日常的開發中經常需要處理字串,而在GO語言中,字串和[]byte是兩種不同的型別。
- 首先來看string的底層定義(src/runtime/string.go):
type stringStruct struct {
str unsafe.Pointer
len int
}
- []byte的底層定義(src/runtime/slice.go):
type slice struct {
array unsafe.Pointer
len int
cap int
}
二者都包含一個指向底層陣列的指標,和底層陣列的長度。不同點在於:
- string是不可變的,一旦建立就不能修改,因此適合用於只讀場景;
- []byte是可變的,可以修改,且包含一個容量資訊(cap);
(注:這裡就不展開slice的擴容機制了,可以參考網上其他資訊)
什麼叫string是不可變的呢?舉個例子:
s := "hello world"
s[0] = 'H' // 編譯錯誤:cannot assign to s[0]
(注:這裡提一嘴go語言中單引號用來表示byte型別,雙引號用來表示string型別)
string不可變的含義是不能修改string底層陣列的某個元素,但我們可以修改string引用的底層陣列:
s := "hello world"
s = "another string"
這時候s的底層陣列已經發生了變化,我們建立了一個新的底層陣列(another string)並將s的指標指向它。原先的底層陣列(hello world)將等待gc回收。
[]byte是可變的,我們可以修改它的元素:
b := []byte{'h', 'e', 'l', 'l', 'o'}
b[0] = 'H'
這時候b的底層陣列中第一個元素已經變成了'H'。
轉換
在實際使用時,我們可能需要將string與[]byte互相轉換,有以下兩種常見的方式:
普通轉換
string轉[]byte:
s := "hello world"
b := []byte(s)
[]byte轉string:
b := []byte{'h', 'e', 'l', 'l', 'o'}
s := string(b)
強轉換 (有風險 謹慎使用)
- 在go版本<1.20中
透過unsafe包和reflect包實現,其主要原理是拿到底層陣列的指標,然後轉換成[]byte或string。
func String2Bytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func Bytes2String(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
注:refelct包中的SliceHeader和StringHeader是切片、string的執行時表現
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
- 在go版本>=1.20中 由於安全性問題reflect包中的StringHeader和SliceHeader已被標註為deprecated,建議使用unsafe包來實現轉換。
// Deprecated: Use unsafe.String or unsafe.StringData instead.
// Deprecated: Use unsafe.Slice or unsafe.SliceData instead.
func String2Bytes(s string) []byte {
// StringData獲取string的底層陣列指標,unsafe.Slice透過指標和長度構建切片
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func Bytes2String(b []byte) string {
// SliceData獲取切片的底層陣列指標,unsafe.String透過指標和長度構建string
return unsafe.String(unsafe.SliceData(b), len(b))
}
注:強轉換可能出現重大問題!!!如下:
str := "hello world"
bs := String2Bytes(str)
bs[0] = 'H'
// str作為string不可修改,bs作為[]byte可修改,透過強轉換二者指向同一個底層陣列
// 修改bs時會出現嚴重錯誤,透過 defer + recover 也不能捕獲
/*
unexpected fault address 0x1dc1f8
fatal error: fault
[signal 0xc0000005 code=0x1 addr=0x1dc1f8 pc=0x1a567b]
*/
兩種轉換的效能對比
轉換函式:
func String2Bytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func Bytes2String(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
func String2Bytes_basic(s string) []byte {
return []byte(s)
}
func Bytes2String_basic(b []byte) string {
return string(b)
}
測試程式碼:
func BenchmarkS2B(b *testing.B) {
str := "hello world hello world hello world hello world "
for i := 0; i < b.N; i++ {
_ = String2Bytes(str)
}
}
func BenchmarkB2S(b *testing.B) {
bs := []byte("hello world hello world hello world hello world ")
for i := 0; i < b.N; i++ {
_ = Bytes2String(bs)
}
}
func BenchmarkS2Bbasic(b *testing.B) {
str := "hello world hello world hello world hello world "
for i := 0; i < b.N; i++ {
_ = String2Bytes_basic(str)
}
}
func BenchmarkB2Sbasic(b *testing.B) {
bs := []byte("hello world hello world hello world hello world ")
for i := 0; i < b.N; i++ {
_ = Bytes2String_basic(bs)
}
}
測試結果:
goos: windows
goarch: amd64
pkg: hello/convert
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkS2B-16 1000000000 0.3371 ns/op 0 B/op 0 allocs/op
BenchmarkB2S-16 1000000000 0.5940 ns/op 0 B/op 0 allocs/op
BenchmarkS2Bbasic-16 48838329 24.82 ns/op 48 B/op 1 allocs/op
BenchmarkB2Sbasic-16 50250835 22.35 ns/op 48 B/op 1 allocs/op
顯然使用強轉換的效能更高,原因在於對於標準轉換,無論是從 []byte 轉 string 還是 string 轉 []byte 都會涉及底層陣列的複製。而強轉換是直接替換指標的指向,從而使得 string 和 []byte 指向同一個底層陣列。當資料長度大於 32 個位元組時,標準轉換需要透過 mallocgc 申請新的記憶體,之後再進行資料複製工作。所以,當轉換資料較大時,兩者效能差距會愈加明顯。
兩種轉換方式的選擇:
- 在你不確定安全隱患的條件下,儘量採用標準方式進行資料轉換。
- 當程式對執行效能有高要求,同時滿足對資料僅僅只有讀操作的條件,且存在頻繁轉換(例如訊息轉發場景),可以使用強轉換。