[Go] string 和 []byte 的轉換分析

前言

爲什麼會有今天這篇文章呢?前天在一個羣裏看到了一份Go語言面試的八股文,其中有一道題就是 "字符串轉成 byte 數組,會發生內存拷貝嗎?";這道題挺有意思的,本質就是在問你string[]byte的轉換原理,考驗你的基本功底。今天我們就來好好的探討一下兩者之間的轉換方式。

byte 類型

我們看一下官方對byte的定義:

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

我們可以看到byte就是uint8的別名,它是用來區分字節值8 位無符號整數值

其實可以把byte當作一個ASCII碼的一個字符。

示例:

var ch byte = 65
var ch byte = '\x41'
var ch byte = 'A'

[]byte類型

[]byte就是一個byte類型的切片,切片本質也是一個結構體,定義如下:

// src/runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

這裏簡單說明一下這幾個字段,array代表底層數組的指針,len代表切片長度,cap代表容量。看一個簡單示例:

func main()  {
 sl := make([]byte,0,2)
 sl = append(sl, 'A')
 sl = append(sl,'B')
 fmt.Println(sl)
}

根據這個例子我們可以畫一個圖:

string 類型

先來看一下string的官方定義:

// 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位字節的集合,通常但不一定代表 UTF-8 編碼的文本。string 可以爲空,但是不能爲 nil。string 的值是不能改變的

看一個簡單的例子:

func main()  {
 str := "asong"
 fmt.Println(str)
}

string類型本質也是一個結構體,定義如下:

type stringStruct struct {
    str unsafe.Pointer
    len int
}

stringStructslice還是很相似的,str指針指向的是某個數組的首地址,len代表的就是數組長度。怎麼和slice這麼相似,底層指向的也是數組,是什麼數組呢?我們看看他在實例化時調用的方法:

//go:nosplit
func gostringnocopy(str *byte) string {
 ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
 s := *(*string)(unsafe.Pointer(&ss))
 return s
}

入參是一個byte類型的指針,從這我們可以看出string類型底層是一個byte類型的數組,所以我們可以畫出這樣一個圖片:

string 和 []byte 有什麼區別

上面我們一起分析了string類型,其實他底層本質就是一個byte類型的數組,那麼問題就來了,string類型爲什麼還要在數組的基礎上再進行一次封裝呢?

這是因爲在Go語言中string類型被設計爲不可變的,不僅是在Go語言,其他語言中string類型也是被設計爲不可變的,這樣的好處就是:在併發場景下,我們可以在不加鎖的控制下,多次使用同一字符串,在保證高效共享的情況下而不用擔心安全問題。

string類型雖然是不能更改的,但是可以被替換,因爲stringStruct中的str指針是可以改變的,只是指針指向的內容是不可以改變的。看個例子:

func main()  {
 str := "song"
 fmt.Printf("%p\n",[]byte(str))
 str = "asong"
 fmt.Printf("%p\n",[]byte(str))
}
// 運行結果
0xc00001a090
0xc00001a098

我們可以看出來,指針指向的位置發生了變化,也就說每一個更改字符串,就需要重新分配一次內存,之前分配的空間會被gc回收。

string 和 []byte 標準轉換

Go語言中提供了標準方式對string[]byte進行轉換,先看一個例子:

func main()  {
 str := "asong"
 by := []byte(str)

 str1 := string(by)
 fmt.Println(str1)
}

標準轉換用起來還是比較簡單的,那你知道他們內部是怎樣實現轉換的嗎?我們來分析一下:

我們對上面的代碼執行如下指令go tool compile -N -l -S ./string_to_byte/string.go,可以看到調用的是runtime.stringtoslicebyte

// runtime/string.go go 1.15.7
const tmpStringBufSize = 32

type tmpBuf [tmpStringBufSize]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
}
// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
func rawbyteslice(size int) ([]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
}

這裏分了兩種狀況,通過字符串長度來決定是否需要重新分配一塊內存。也就是說預先定義了一個長度爲32的數組,字符串的長度超過了這個數組的長度,就說明[]byte不夠用了,需要重新分配一塊內存了。這也算是一種優化吧,32是閾值,只有超過32纔會進行內存分配。

最後我們會通過調用copy方法實現 string 到 []byte 的拷貝,具體實現在src/runtime/slice.go中的slicestringcopy方法,這裏就不貼這段代碼了,這段代碼的核心思路就是:將 string 的底層數組從頭部複製 n 個到 []byte 對應的底層數組中去

[]byte類型轉換到string類型本質調用的就是runtime.slicebytetostring

// 以下無關的代碼片段
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
 if n == 0 {
  return ""
 }
 if n == 1 {
  p := unsafe.Pointer(&staticuint64s[*ptr])
  if sys.BigEndian {
   p = add(p, 7)
  }
  stringStructOf(&str).str = p
  stringStructOf(&str).len = 1
  return
 }

 var p unsafe.Pointer
 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
 memmove(p, unsafe.Pointer(ptr), uintptr(n))
 return
}

這段代碼我們可以看出會根據[]byte的長度來決定是否重新分配內存,最後通過memove可以拷貝數組到字符串。

string 和 []byte 強轉換

標準的轉換方法都會發生內存拷貝,所以爲了減少內存拷貝和內存申請我們可以使用強轉換的方式對兩者進行轉換。在標準庫中有對這兩種方法實現:

// runtime/string.go
func slicebytetostringtmp(ptr *byte, n int) (str string) {
 stringStructOf(&str).str = unsafe.Pointer(ptr)
 stringStructOf(&str).len = n
 return
}

func stringtoslicebytetmp(s string) []byte {
    str := (*stringStruct)(unsafe.Pointer(&s))
    ret := slice{array: unsafe.Pointer(str.str), len: str.len, cap: str.len}
    return *(*[]byte)(unsafe.Pointer(&ret))
}

通過這兩個方法我們可知道,主要使用的就是unsafe.Pointer進行指針替換,爲什麼這樣可以呢?因爲stringslice的結構字段是相似的:

type stringStruct struct {
    str unsafe.Pointer
    len int
}
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

唯一不同的就是cap字段,arraystr是一致的,len是一致的,所以他們的內存佈局上是對齊的,這樣我們就可以直接通過unsafe.Pointer進行指針替換。

兩種轉換如何取捨

當然是推薦大家使用標準轉換方式了,畢竟標準轉換方式是更安全的!但是如果你是在高性能場景下使用,是可以考慮使用強轉換的方式的,但是要注意強轉換的使用方式,他不是安全的,這裏舉個例子:

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

func main()  {
 str := "hello"
 by := stringtoslicebytetmp(str)
 by[0] = 'H'
}

運行結果:

unexpected fault address 0x109d65f
fatal error: fault
[signal SIGBUS: bus error code=0x2 addr=0x109d65f pc=0x107eabc]

我們可以看到程序直接發生嚴重錯誤了,即使使用defer+recover也無法捕獲。原因是什麼呢?

我們前面介紹過,string類型是不能改變的,也就是底層數據是不能更改的,這裏因爲我們使用的是強轉換的方式,那麼by指向了str的底層數組,現在對這個數組中的元素進行更改,就會出現這個問題,導致整個程序down掉!

總結

本文我們一起分析bytestring類型的基本定義,也分析了[]bytestring的兩種轉換方式,應該還差最後一環,也就是大家最關心的性能測試,這個我沒有做,我覺得沒有很大意義,通過前面的分析就可以得出結論,強轉換的方式性能肯定要比標準轉換要好。對於這兩種方式的使用,大家還是根據實際場景來選擇,脫離場景的談性能就是耍流氓!!!

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