GO語言中string和[]byte的區別及轉換

CLoud11y發表於2024-12-01

區別

在我們日常的開發中經常需要處理字串,而在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 申請新的記憶體,之後再進行資料複製工作。所以,當轉換資料較大時,兩者效能差距會愈加明顯。

兩種轉換方式的選擇:

  • 在你不確定安全隱患的條件下,儘量採用標準方式進行資料轉換。
  • 當程式對執行效能有高要求,同時滿足對資料僅僅只有讀操作的條件,且存在頻繁轉換(例如訊息轉發場景),可以使用強轉換。

相關文章