你能一口說出go中字符串轉字節切片的容量嘛?

2020-12-17 新世界雜貨鋪

神奇的現象

切片,切片,又是切片!

前一篇文章切片真的是引用類型嘛?講的是切片, 今天遇到的神奇問題還是和切片有關, 具體怎麼個神奇法, 讓我們來看看下面幾個現象

現象一

現象二

現象三

現象四

現象五

分析

到這兒我已經滿腦子問號了

字符串變量轉切片

一個小小的字符串轉切片, 內部究竟發生了什麼, 竟然如此的神奇。 這種時候只好祭出前一篇文章的套路了, 看看彙編代碼(希望之後有機會能夠對go的彙編語法進行簡單的介紹)有沒有什麼關鍵詞能夠幫助我們

以下為現象一轉換的彙編代碼關鍵部分

以下為現象二轉換的彙編代碼關鍵部分

在看彙編代碼之前, 我們首先來看一看runtime.stringtoslicebyte的函數籤名

到這裡只靠關鍵詞已經無法看出更多的信息了,還是需要稍微了解一下彙編的語法,筆者在這裡列出一點簡單的分析, 之後我們還是可以通過取巧的方法發現更多的東西

通過上面彙編代碼的分析可以知道,現象一和現象二的區別就是傳遞給runtime.stringtoslicebyte的第一個參數不同。 通過對runtime包中stringtoslicebyte函數分析,第一個參數是否有值和字符串長度會影響代碼執行的分支,從而生成不同的切片, 因此容量不一樣也是常理之中, 下面我們看源碼

然而, stringtoslicebyte的第一個參數什麼情況下才會有值,什麼情況下為nil, 我們仍然不清楚。那怎麼辦呢, 只好祭出全局搜索大法:

最終在go的編譯器源碼cmd/compile/internal/gc/walk.go發現了如下代碼塊

我們查看mkcall函數籤名可以知道, 從第四個參數開始的所有變量都會作為參數傳遞給第一個參數對應的函數, 最後生成一個*Node的變量。其中Node結構體解釋如下:

綜合上述信息我們得出的結論是,編譯器會對stringtoslicebyte的函數調用生成一個AST(抽象語法樹)對應的節點。因此我們也知道傳遞給stringtoslicebyte函數的第一個變量也就對應於上圖中的變量a.

其中a的初始值為nodnil()的返回值,即默認為nil. 但是n.Esc == EscNone時,a會變成一個數組。我們看一下EscNone的解釋.

由上可知, EscNone用來判斷變量是否逃逸,到這兒了我們就很好辦了,接下來我們對現象一和現象二的代碼進行逃逸分析.

根據上面的信息我們知道在現象一中,bs變量發生了逃逸,現象二中變量未發生逃逸,也就是說stringtoslicebyte函數的第一個參數在變量未發生逃逸時其值不為nil,變量發生逃逸時其值為nil。到這裡我們已經搞明白stringtoslicebyte的第一個參數了, 那我們繼續分析stringtoslicebyte的內部邏輯

我們在runtime/string.go中看到stringtoslicebyte第一個參數的類型定義如下:

綜上: 現象二中bs變量未發生變量逃逸, stringtoslicebyte第一個參數不為空且是一個長度為32的byte數組, 因此在現象二中生成了一個容量為32的切片

根據對stringtoslicebyte的源碼分析, 我們知道現象一調用了rawbyteslice函數

由上面的代碼知道, 切片的容量通過runtime/msize.go中的roundupsize函數計算得出, 其中_MaxSmallSizeclass_to_size均定義在runtime/sizeclasses.go

由於字符串abc的長度小於_MaxSmallSize(32768),故意切片的長度只能取數組class_to_size中的值, 即0, 8, 16, 32, 48, 64, 80, 96, 112, 128....

至此, 現象一中切片容量為什麼為8也真相大白了。相信到這裡很多人已經明白現象四和現象五是怎麼回事兒了, 其邏輯分別與現象一和現象二是一致的, 有興趣的, 可以在自己的電腦上面試一試。

字符串直接轉切片

相信有人問了,你說了這麼多, 現象三還是不能解釋啊。請各位看官莫急, 接下來我們繼續分析。

各位細心的小夥伴應該早就發現了我們在上面的cmd/compile/internal/gc/walk.go

源碼圖中摺疊了部分代碼, 現在我們就將這塊神秘的代碼赤裸裸的展示出來

我們分析這塊代碼發現,go編譯器在將字符串轉字節切片生成AST時,總共分為三步。

先判斷該變量是否是常量字符串,如果是常量字符串,則直接通過types.NewArray創建一個和字符串等長的數組常量字符串生成的切片變量也要進行逃逸分析,並判斷其大小是否大於函數棧允許分配給變量的最大長度, 從而判斷節點是分配在棧上還是在堆上最後,如果字符串長度是大於0, 將字符串內容複製到字節切片中, 然後返回。因此現象三中的切片容量是3也就完全清楚了結論

字符串轉字節切片步驟如下

判斷是否是常量, 如果是常量則轉換為等容量等長的字節切片如果是變量, 先判斷生成的切片是否發生變量逃逸如果逃逸或者字符串長度>32, 則根據字符串長度可以計算出不同的容量如果未逃逸且字符串長度<=32, 則字符切片容量為32擴展

常見逃逸情況

函數返回局部指針棧空間不足逃逸動態類型逃逸, 很多函數參數為interface類型,比如fmt.Println(a ...interface{}),編譯期間很難確定其參數的具體類型, 也會發生逃逸閉包引用對象逃逸注: 寫本文時, 筆者所用go版本為: go1.13.4

參考

https://golang.org/src/cmd/compile/README.md

https://my.oschina.net/renhc/blog/2222104

https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/walk.go#L1522

https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/esc.go#L39

https://github.com/golang/go/blob/master/src/runtime/string.go#L1522

https://github.com/golang/go/blob/master/src/runtime/msize.go

https://github.com/golang/go/blob/master/src/runtime/sizeclasses.go#L75

生命不息, 探索不止, 後續將持續更新有關於go的技術探索

原創不易, 卑微求關注收藏二連.

相關焦點

  • go runtime debug 小技巧
    這令我很不開心, 雖然C/C++應用很廣泛, 但是我對它真的沒啥興趣啊, 對它相關的調試工具就更加不感冒了, 雖然它可以調試go程序, 但是總感覺心裡少了點什麼, 難道dlv它不香嘛, 於是就有了今天這篇文章。dlv命令行debugdlv的名頭應該不用我多說, 所以我們直奔主題1.
  • 深入剖析go中字符串的編碼問題——特殊字符的string怎麼轉byte?
    go中的字符眾所周知,go中能表示字符的有兩種類型,分別是byte和rune,byte和rune的定義分別是:type byte = uint8和type rune = int32。我們通過go對二進位轉為整型:綜上:當用字符轉字節時輸出的是字符本身的整型值,當用字符串轉字節切片時,實際上是輸出的是utf8的字節切片序列(go中的字符串存儲的就是utf8位元組切片)。此時,我們回顧一下最開始的問題,就會發現輸出是完全符合預期的。
  • Rob Pike 帶你重新認識字符串、字節、rune和字符
    介紹上一篇博客文章使用許多示例說明了切片在其實現背後的機制,從而說明了切片在 Go 中的工作方式。以此為背景,本文會討論 Go 中的字符串。什麼是字符串?讓我們從一些基礎知識開始。在 Go 中,字符串實際上是只讀的字節切片。如果你完全不知道一個字節切片是什麼以及它是如何工作的,請閱讀上一篇博客文章 ; 我們在這裡假設你已經知道這些。預先說明字符串可以包含任意字節很重要,字符串沒有規定只能包含 Unicode 文本,UTF-8 文本或任何其他預定義格式。
  • go 的數組還是切片都沒有什麼不一樣
    byte 是最基礎字節類型,是 uint8 類型的別名,而 rune 是 Go 中的字符類型,是 int32 的別名.最常用的字符串類型 string 應該不用介紹了吧?由於參數傳遞只有值傳遞一種方式,因此推測切片內部肯定存在指針,參數傳遞時傳遞的是指針,所以函數內部的修改才能影響到到函數外部的變量.slice 的內部實現中有三個變量,指針 ptr,個數 len 和容量 cap ,其中 ptr 指向真正的數據存儲地址.
  • Go 語言基於 channel 實現的並發安全的字節池
    字節切片[]byte是我們在編碼中經常使用到的,比如要讀取文件的內容,或者從io.Reader獲取數據等,都需要[]byte做緩衝。從這裡也可以看到,結構體中定義的w和wcap欄位,用於make函數的len和cap參數。有了Get方法,還要有Put方法,這樣就可以把使用過的[]byte放回字節池,便於重用。
  • Go 中 string 轉 []byte 的陷阱
    上面2.2章節例子中輸出的是:32,0。看來問題關鍵在這裡,兩者差別在於一個是默認 []byte{},另外個是空字符串轉的 []byte("")。其長度都是 0,比較好理解,但為什麼容量是 32 就不符合預期輸出了?因為 capacity 是數組還能添加多少的容量,在能滿足的情況,不會重新分配。所以 capacity-length=32,是足夠 append a,b的。
  • Go 高效截取字符串的一些思考
    字節切片截取 這正是 「hollowaykeanho」 給出的第一個方案,我想也是很多人想到的第一個方案,利用 go 的內置切片語法截取字符串:s := "abcdef"fmt.Println(s[1:4])
  • Go語言學習筆記(五):切片操作
    前言前面幾篇文章主要對Go中一些數據類型的實現原理進行了思考,這一篇來研究一下切片的一些實際操作方法。
  • Go語言 | 基於channel實現的並發安全的字節池
    字節切片[]byte是我們在編碼中經常使用到的select+chan的方式,select+chan,[]byte的長度和容量都是
  • go中統計中文字符個數的兩種方式,它們的性能如何?
    引言在go中應該有很多種方法來統計字符串中中文字符的個數。下面介紹兩種方式,一種是使用正則表達式來匹配,需要使用 regexp 包;另一種是用標準庫中 unicode 包自帶的 unicode.Is 方法。
  • 二維碼最大容量是多少 1108個字節可信嗎?
    那麼,今天小編將自己知道的關於「二維碼」的信息,統統告訴大家,希望能為各位網友帶來幫助!二維碼的起源與發展  國外對於二維碼技術的研究開始於20世紀80年代末,在符號表示技術上已經研製出多種碼制,比較常見的有PDF417、QR Code、Code 49、Code 16K、Code One等。
  • 人人都能懂的go語言教程——字符串篇
    除了像是數組一樣,支持下標的訪問之外,go中的字符串還支持拼接以及求長度的操作。原因很簡單,因為在utf-8編碼當中,一個漢字需要3個字節編碼。那如果我們想要得到字符串本身的長度,而不是字符串佔據的字節數,應該怎麼辦呢?這個時候,我們需要用到一個新的結構叫做rune,它表示單個Unicode字符。所以我們可以將string轉化成rune數組,之後再來計算長度,得到的結果就準確了。
  • Python中的變量與字符串數據類型
    字符串字符串是由任意字節的字符組成的,主要是由單引號' ',雙引號" ",三引號""" """成對表示的。1.2切片(Slice)切片的方式與單索引讀取方式相同但是可以獲取字符串中的一部分元素。[起始位置:終止位置:步長]比如我們想要從name變量中提取出傑瑞是指老鼠,就應該這麼切。
  • 計算機中的IO字符流
    計算機中按照流的概念主要分為:輸入流和輸出流。上一文章了解到字節流。現在來了解一下字符流。除了字節流和字符流之外,還有以下幾個流:轉換流,緩衝流,標準輸入輸出流,數據流等。字符流一次只處理一個字符,根據流向可分為字符輸入流和字符輸出流。
  • 一文搞定Go語言語法
    一個 rune 的類型值即可表示一個 Unicode 字符。一個 Unicode 代碼點通常由 "U+" 和一個以十六進位表示法表示的整數表示。字符串類型字符串 的表示法有兩種,即:原生表示法和解釋型表示法。原生表示法,需用用反引號 "`" 把字符序列包起來,如果用解釋型表示法,則需要用雙引號 """ 包裹字符序列。
  • Go語言的學習筆記(第二章)
    字符串變量的默認值為空字符串。布爾型變量默認為false。切片、函數、指針變量的默認為nil。當然我們也可在聲明變量的時候為其指定初始值。const中每新增一行常量聲明將使iota計數一次(iota可理解為const語句塊中的行索引)。使用iota能簡化定義,在定義枚舉時很有用。
  • 你真的知道 Python的 字符串是什麼嗎?
    i:j] # s切片從第i項到第j-1項s[i:j:k]  #  s切片從第i項到第j-1項,間隔為klen(s)  # s的長度min(s)  # s的最小元素max(s)  # s的最大元素s.index(x) # x的索引位置s.count(x)  # s中出現x的總次數字符串序列還具備一些特有的操作,限於篇幅,按下不表
  • Python高效編程之88條軍規(1):編碼規範、字節序列與字符串
    用程式語言寫代碼是自由的,編譯器不會強制你使用特定的格式編寫程序(只要符合語法,編譯器才不管你呢!)。所以很多程式設計師就會將Python當做自己熟悉的Java、C++等語言來用。不過這些編碼方式真的是最好的選擇嗎?本系列文章將為你揭秘88種在編寫Python代碼中的規則,這些規則將會讓你Python程序更加健壯,運行效率更高。
  • C++中字符編碼的轉換(Unicode、UTF-8、ANSI)
    我們要做到能在Unicode、UTF-8、ANSI這三種編碼格式中自由轉換。如下圖所示:在C++中,要怎麼做呢?這個類在#include <codecvt>中。有了wstring_convert提供寬字符字符串到多字節字符串的轉化,而這個轉換規則由codecvt_uft8提供。這樣子就可以實現UTF8和Unicode的互相轉換。
  • Go語言字符串
    在Go語言中,沒有字符類型,字符類型是rune類型,rune是int32的別稱。可使用 []byte() 獲取字節,使用 []rune() 獲取字符,可對中文進行轉換。在已有字符串數組的場合,使用 strings.Join() 能有比較好的性能;2. 在一些性能要求較高的場合,儘量使用 buffer.WriteString() 以獲得更好的性能;3. "+" 運算符在較少字符串連接的場景下性能最好,而且代碼更簡短清晰,可讀性更好;4.