1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        面試官:你能聊聊 string 和 []byte 的轉(zhuǎn)換嗎?

        共 7817字,需瀏覽 16分鐘

         ·

        2022-06-30 09:41

        前言

        為什么會有今天這篇文章呢?前天在一個群里看到了一份Go語言面試的八股文,其中有一道題就是"字符串轉(zhuǎn)成byte數(shù)組,會發(fā)生內(nèi)存拷貝嗎?";這道題挺有意思的,本質(zhì)就是在問你string[]byte的轉(zhuǎn)換原理,考驗(yàn)?zāi)愕幕竟Φ?。今天我們就來好好的探討一下兩者之間的轉(zhuǎn)換方式。

        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的別名,它是用來區(qū)分字節(jié)值8位無符號整數(shù)值

        其實(shí)可以把byte當(dāng)作一個ASCII碼的一個字符。

        示例:

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

        []byte類型

        []byte就是一個byte類型的切片,切片本質(zhì)也是一個結(jié)構(gòu)體,定義如下:

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

        這里簡單說明一下這幾個字段,array代表底層數(shù)組的指針,len代表切片長度,cap代表容量??匆粋€簡單示例:

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

        根據(jù)這個例子我們可以畫一個圖:

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

        看一個簡單的例子:

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

        string類型本質(zhì)也是一個結(jié)構(gòu)體,定義如下:

        type stringStruct struct {
            str unsafe.Pointer
            len int
        }

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

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

        入?yún)⑹且粋€byte類型的指針,從這我們可以看出string類型底層是一個byte類型的數(shù)組,所以我們可以畫出這樣一個圖片:

        string和[]byte有什么區(qū)別

        上面我們一起分析了string類型,其實(shí)他底層本質(zhì)就是一個byte類型的數(shù)組,那么問題就來了,string類型為什么還要在數(shù)組的基礎(chǔ)上再進(jìn)行一次封裝呢?

        這是因?yàn)樵?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">Go語言中string類型被設(shè)計為不可變的,不僅是在Go語言,其他語言中string類型也是被設(shè)計為不可變的,這樣的好處就是:在并發(fā)場景下,我們可以在不加鎖的控制下,多次使用同一字符串,在保證高效共享的情況下而不用擔(dān)心安全問題。

        string類型雖然是不能更改的,但是可以被替換,因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">stringStruct中的str指針是可以改變的,只是指針指向的內(nèi)容是不可以改變的。看個例子:

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

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

        string和[]byte標(biāo)準(zhǔn)轉(zhuǎn)換

        Go語言中提供了標(biāo)準(zhǔn)方式對string[]byte進(jìn)行轉(zhuǎn)換,先看一個例子:

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

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

        標(biāo)準(zhǔn)轉(zhuǎn)換用起來還是比較簡單的,那你知道他們內(nèi)部是怎樣實(shí)現(xiàn)轉(zhuǎn)換的嗎?我們來分析一下:

        • string類型轉(zhuǎn)換到[]byte類型

        我們對上面的代碼執(zhí)行如下指令go tool compile -N -l -S ./string_to_byte/string.go,可以看到調(diào)用的是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) (b []byte) {
         cap := roundupsize(uintptr(size))
         p := mallocgc(capnilfalse)
         if cap != uintptr(size) {
          memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
         }

         *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
         return
        }

        這里分了兩種狀況,通過字符串長度來決定是否需要重新分配一塊內(nèi)存。也就是說預(yù)先定義了一個長度為32的數(shù)組,字符串的長度超過了這個數(shù)組的長度,就說明[]byte不夠用了,需要重新分配一塊內(nèi)存了。這也算是一種優(yōu)化吧,32是閾值,只有超過32才會進(jìn)行內(nèi)存分配。

        最后我們會通過調(diào)用copy方法實(shí)現(xiàn)string到[]byte的拷貝,具體實(shí)現(xiàn)在src/runtime/slice.go中的slicestringcopy方法,這里就不貼這段代碼了,這段代碼的核心思路就是:將string的底層數(shù)組從頭部復(fù)制n個到[]byte對應(yīng)的底層數(shù)組中去

        • []byte類型轉(zhuǎn)換到string類型

        []byte類型轉(zhuǎn)換到string類型本質(zhì)調(diào)用的就是runtime.slicebytetostring

        // 以下無關(guān)的代碼片段
        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), nilfalse)
         }
         stringStructOf(&str).str = p
         stringStructOf(&str).len = n
         memmove(p, unsafe.Pointer(ptr), uintptr(n))
         return
        }

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

        string和[]byte強(qiáng)轉(zhuǎn)換

        標(biāo)準(zhǔn)的轉(zhuǎn)換方法都會發(fā)生內(nèi)存拷貝,所以為了減少內(nèi)存拷貝和內(nèi)存申請我們可以使用強(qiáng)轉(zhuǎn)換的方式對兩者進(jìn)行轉(zhuǎn)換。在標(biāo)準(zhǔn)庫中有對這兩種方法實(shí)現(xiàn):

        // 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.lencap: str.len}
            return *(*[]byte)(unsafe.Pointer(&ret))
        }

        通過這兩個方法我們可知道,主要使用的就是unsafe.Pointer進(jìn)行指針替換,為什么這樣可以呢?因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">string和slice的結(jié)構(gòu)字段是相似的:

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

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

        兩種轉(zhuǎn)換如何取舍

        當(dāng)然是推薦大家使用標(biāo)準(zhǔn)轉(zhuǎn)換方式了,畢竟標(biāo)準(zhǔn)轉(zhuǎn)換方式是更安全的!但是如果你是在高性能場景下使用,是可以考慮使用強(qiáng)轉(zhuǎn)換的方式的,但是要注意強(qiáng)轉(zhuǎn)換的使用方式,他不是安全的,這里舉個例子:

        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'
        }

        運(yùn)行結(jié)果:

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

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

        我們前面介紹過,string類型是不能改變的,也就是底層數(shù)據(jù)是不能更改的,這里因?yàn)槲覀兪褂玫氖菑?qiáng)轉(zhuǎn)換的方式,那么by指向了str的底層數(shù)組,現(xiàn)在對這個數(shù)組中的元素進(jìn)行更改,就會出現(xiàn)這個問題,導(dǎo)致整個程序down掉!

        總結(jié)

        本文我們一起分析bytestring類型的基本定義,也分析了[]bytestring的兩種轉(zhuǎn)換方式,應(yīng)該還差最后一環(huán),也就是大家最關(guān)心的性能測試,這個我沒有做,我覺得沒有很大意義,通過前面的分析就可以得出結(jié)論,強(qiáng)轉(zhuǎn)換的方式性能肯定要比標(biāo)準(zhǔn)轉(zhuǎn)換要好。對于這兩種方式的使用,大家還是根據(jù)實(shí)際場景來選擇,脫離場景的談性能就是耍流氓?。?!

        瀏覽 56
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            精品无码人妻 | 偷拍一女多男做爰免费视频 | 天天艹日日干 | 激情婷婷五月天 | 国产青青操 | 国产精品久久久久久久久大全 | 姐姐说家里没人可以让孩子成长 | 91麻豆精品国产自产在线游戏 | 亚洲无码123 | 欧美色成一类片视频 |