最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

深入string理解Golang是怎樣實現(xiàn)的

 更新時間:2023年04月23日 09:46:25   作者:卡布奇諾賽高  
這篇文章主要為大家介紹了深入string理解Golang是怎樣實現(xiàn)的原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

本身打算先寫完sync包的, 但前幾天在復習以前筆記的時候突然發(fā)現(xiàn)與字符串相關(guān)的寥寥無幾. 同時作為一個Java選手, 很輕易的想到了幾個問題

  • go字符串存儲于內(nèi)存的哪部分區(qū)域?
  • 我們初始化兩個"hello world", 這兩個"hello world"會放到同一塊內(nèi)存空間嗎?
  • go字符串是動態(tài)的還是靜態(tài)的, 修改他的時候是修改原字符串還是新構(gòu)建一個字符串?

在網(wǎng)上搜索后發(fā)現(xiàn)目前網(wǎng)上對go語言字符串的介紹相關(guān)甚少, 因此我在仔細閱讀源碼后產(chǎn)出了這批文章.

ps: 本文雖由Java中問題引出, 但后續(xù)內(nèi)容和Java無關(guān), 碼字不易, 對你有幫助的話麻煩幫忙點個贊^_^.

內(nèi)容介紹

本文將介紹如下內(nèi)容

字符串數(shù)據(jù)結(jié)構(gòu)

字符串中的數(shù)據(jù)結(jié)構(gòu)如下

type stringStruct struct {
   str unsafe.Pointer
   len int
}
  • str: 大部分情況下指向只讀數(shù)據(jù)段中的一塊內(nèi)存區(qū)域, 少部分情況指向堆/棧, unsafe.Pointer類型, 大小8字節(jié).
  • len: 這個字符串的長度, int類型, 在64bit機上大小8字節(jié), 在32bit機上大小4字節(jié).

字符串會分配到內(nèi)存中的哪塊區(qū)域

我們先看下這張圖, 下面內(nèi)容結(jié)合本圖理解

我們把字符串分為兩種

  • 編譯期即可確定的字符串, 如a:="hello"
  • 運行時通過+拼接得到的字符串, 如b:=a+"world"

編譯期即可確定的字符串

a := "hello world"

我們這里把字符串占用的內(nèi)存分為兩部分

  • stringStruct結(jié)構(gòu)體所在的內(nèi)存
  • unsafe.Pointer類型的str所在的內(nèi)存

首先是stringStruct, 他是一個16字節(jié)大小的結(jié)構(gòu)體, 因此他和一個普通結(jié)構(gòu)體一樣, 根據(jù)逃逸分析判斷是否可以分配在棧上, 如果不行, 也會根據(jù)分級分配的方式分配到堆中.

而str則是指向了.rodata(只讀數(shù)據(jù)段)中的存放的字符串字面量, 因此字符串字面量是在.rodata中

綜上: string的數(shù)據(jù)結(jié)構(gòu)stringStruct分配在堆/棧中, 而他對應的字符串字面量則是在只讀數(shù)據(jù)段中

如果我們創(chuàng)建兩個hello world字符串, 他們會放到同一內(nèi)存區(qū)域嗎?

根據(jù)上面的分析, 我們可以很容易的得到答案, 他們的數(shù)據(jù)結(jié)構(gòu)stringStruct會分配在堆/棧的不同內(nèi)存空間中, 而unsafe.Pointer則指向.rodata中的同一塊內(nèi)存區(qū)域

我們可以做出如下驗證方式

//因為stringStruct是runtime包下一個不對外暴露的數(shù)據(jù)結(jié)構(gòu),
//所以我們新建一個結(jié)構(gòu)相同的數(shù)據(jù)結(jié)構(gòu)來接收string的內(nèi)容
type Reception struct {
   p unsafe.Pointer
   len int
}
func main(){
   a := "hello world"
   b := "hello world"
   //用新建的Reception接收字符串內(nèi)容, 本質(zhì)上就是把a/b對應的二進制數(shù)據(jù)重新解析為Reception,
   //而Reception和stringStruct的結(jié)構(gòu)相同, 所以不會出問題.
   rA := *(*Reception)(unsafe.Pointer(&a))
   rB := *(*Reception)(unsafe.Pointer(&b))
   //輸出a,b的地址
   fmt.Println(&a)
   fmt.Println(&b)
   //輸出stringStruct的str指向的地址
   fmt.Println(rA.p)
   fmt.Println(rB.p)
}

我們得到了如下結(jié)果

0xc000050260
0xc000050270
0x595700
0x595700

a,b兩個stringStruct被分配到不同地址, 而他們的str則指向了同一地址.

運行時通過+拼接的字符串會放到那塊內(nèi)存中

字面量是否會在編譯器合并

func main(){
   he := "hello"
   //編譯期"li","hua"未能合并
   str1 := he+"li"+"hua"
   //編譯期被合并為"nihao"
   str2 := "ni"+"hao"
   fmt.Println(str1)
}

網(wǎng)上有的文章說, 字符串字面量會在編譯期進行合并, 但我在SDK1.18.9下測試的結(jié)果是只有右值為純字面量時, 才會合并.

我們使用go tool compile -m main.go命令分析, 結(jié)果如下

main.go:8:13: inlining call to fmt.Println
//如果合并的話, 應該是he+"lihua"
main.go:7:17: he + "li" + "hua" escapes to heap
main.go:8:13: ... argument does not escape
main.go:8:13: str1 escapes to heap

大家可以自己用上述命令分析下自己SDK版本是否會合并.

不過重要的是, 我們知道右值為純字面量拼接的字符串會在編譯期合并, 等價于右值為純字面量的字符串, 他的分配方式和編譯期可確定的字符串一致.

接下來我們討論右值表達式中存在變量的情況下是如何進行內(nèi)存分配的

當我們用+連接多個字符串時, 會發(fā)生什么

我們先說結(jié)論, 運行時通過+連接多個字符串構(gòu)成新串, 新串的stringStruct結(jié)構(gòu)體和str指向的字面量都會被分配到堆/??臻g中.

在go語言編譯期, 會把字符串的"+"替換為func concatstrings(buf *tmpBuf, a []string) string函數(shù).

分配到棧上還是堆上

我們看下concatstrings的兩個參數(shù), 其中buf是一個棧空間的內(nèi)存, go語言會通過所有要拼接的字符串總長度以及逃逸分析確定這個字符串會不會分配到棧上, 如果要分配到棧上, 則會傳來buf參數(shù).

棧上分配和堆上分配的流程幾乎一致, 只不過在內(nèi)存分配的時候會根據(jù)buf!=nil來判斷該存放到哪塊內(nèi)存空間而已, 因此下文中我們統(tǒng)一按堆分配介紹.

而第二個參數(shù)a中存儲有全部需要通過"+"連接的字符串

concatstrings函數(shù)執(zhí)行流程如下

  • 用for range循環(huán)來遍歷整個a數(shù)組, 計算其中所有非空串的個數(shù)count和長度總和l
  • 然后調(diào)用func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte)函數(shù)來為這個字符串分配內(nèi)存空間, 并返回字符串和其底層的[]byte數(shù)組. 對于該函數(shù)來說, 如果buf!=nil則使用buf的內(nèi)存空間, 否則調(diào)用func rawstring(size int) (s string, b []byte) 函數(shù), rawstring函數(shù)會調(diào)用mallocgc來在堆上分配內(nèi)存空間, 并返回使用該內(nèi)存空間的字符串及其底層切片.
  • 此時我們已經(jīng)拿到了一個字符串及其底層切片, 因為字符串不可變, 所以go通過修改其底層數(shù)組來為字符串賦值, 他會再次for range循環(huán)a數(shù)組, 然后通過copy函數(shù)來把a中的字符串拷貝到新串對應的底層數(shù)組b中, 從而達到修改新串的目的.
  • 至此, 字符串s的內(nèi)存分配和初始化已經(jīng)全部完成, rawstringtmp函數(shù)返回

這樣我們就得到了一個全部內(nèi)存空間都分配在堆/棧中的字符串.

因此, 即使運行時多個通過+連接而成的新串有著相同的字面量, 他們的str也會指向不同的內(nèi)存空間

驗證

我們可以繼續(xù)把字符串轉(zhuǎn)換為Reception來看看他的str執(zhí)行的地址

//因為stringStruct是runtime包下一個不對外暴露的數(shù)據(jù)結(jié)構(gòu),
//所以我們新建一個結(jié)構(gòu)相同的數(shù)據(jù)結(jié)構(gòu)來接收string的內(nèi)容
type Reception struct {
   p unsafe.Pointer
   len int
}
func main(){
   h := "hello"
   a := h+" world"
   b := h+" world"
   //用新建的Reception接收字符串內(nèi)容, 本質(zhì)上就是把a/b對應的二進制數(shù)據(jù)重新解析為Reception,
   //而Reception和stringStruct的結(jié)構(gòu)相同, 所以不會出問題.
   rA := *(*Reception)(unsafe.Pointer(&a))
   rB := *(*Reception)(unsafe.Pointer(&b))
   //輸出a,b的地址
   fmt.Println(&a)
   fmt.Println(&b)
   //輸出stringStruct的str指向的地址
   fmt.Println(rA.p)
   fmt.Println(rB.p)
}

結(jié)果如下

0xc000050260
0xc000050270
0xc00000a0e0
0xc00000a0f0

a和b字符串的str字段指向堆中不同的內(nèi)存區(qū)域.

rawstring函數(shù)

rawstring真的是一個十分有趣的函數(shù), 因此我決定對他進行詳細的分析, 但他相對有點難度, 如果靜下心來讀懂, 定能讓您有所收獲. 我們直接上源碼逐行分析

func rawstring(size int) (s string, b []byte) {
   //在堆中申請內(nèi)存
   p := mallocgc(uintptr(size), nil, false)
   //把string轉(zhuǎn)換為stringStruct數(shù)據(jù)結(jié)構(gòu)
   stringStructOf(&s).str = p
   stringStructOf(&s).len = size
   //最重要的部分, 讓b重新指向p空間
   *(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}
   return
}
func stringStructOf(sp *string) *stringStruct {
   return (*stringStruct)(unsafe.Pointer(sp))
}

stringStructOf函數(shù)十分簡單, 因為string和stringStruct的結(jié)構(gòu)完全相同, 因此他直接通過把(*stringStruct)(unsafe.Pointer(sp))來把字符串指針sp轉(zhuǎn)換為stringStruct指針, 然后通過stringStruct指針來獲取stringStruct結(jié)構(gòu)體.

我們可以這樣理解下轉(zhuǎn)換方式.

  • sp是一個string類型的指針, 他指向一塊內(nèi)存區(qū)域, 這塊內(nèi)存區(qū)域中全是二進制bit流, 但是我們會安裝string的形式解釋他, 即前8位被解釋成一個指針, 后8位被解釋成一個int類型.
  • 我們把sp轉(zhuǎn)換為一個unsafe.Pointer, 此時將只保留起始地址和長度
  • 然后我們再把sp轉(zhuǎn)換為stringStruct, 因此會按stringStruct的方式解釋這段二進制bit流, 而因為stringStruct的結(jié)構(gòu)和string一樣, 所以也會把前8位解釋成一個指針, 后8位解釋成一個int類型, 不會出現(xiàn)差錯.

接下來我們按同樣的思路看下*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}

  • 首先獲取到b的地址, 然后把他轉(zhuǎn)換為一個*slice
  • 然后通過取地址運算符來獲取slice對應的slice
  • 又因為slice本身就是指針類型, 所以我們讓這個slice=slice{p,size,size}的時候只是改變了其指向, 也就等價于讓b改變指向, 使其指向p這塊內(nèi)存空間, 也就是str指向的那塊內(nèi)存空間.

只會我們就可以通過b來修改這塊內(nèi)存空間, 從而間接修改字符串的ne

go中字符串是不可變的嗎, 我們?nèi)绾蔚玫揭粋€可變的字符串

go中字符串在語義中是不可變的, 并且咱們對字符串進行+操作時也是新開辟一塊內(nèi)存空間來存放修改后的字符串, 真的沒有什么辦法改變一個字符串中的數(shù)據(jù)嗎?

回顧下我們之前分析的結(jié)論

  • 對于編譯期確定的字符串, 他的str指針指向一個.rodata區(qū)的字面量, 不會被改變.
  • 而運行時確定的字符串, 他的str指針指向一個堆棧中的空間, 我們可以讓一個[]byte指向其底層內(nèi)存空間從而間接改變其內(nèi)容

對于編譯期確定的字符串, 嘗試修改.rodata區(qū)中的字面量會panic

//嘗試修改.rodata區(qū)中數(shù)據(jù), painic
func main(){
   str := "hello world"
   byteArr := *(*[]byte)(unsafe.Pointer(&str))
   byteArr[0] = 'w'
   fmt.Println(str)
}

而對于運行時通過+拼接得到的新串, 修改堆棧中存放的字面量則可以成功

//輸出wello world
func main(){
   str := "hello"
   //此時字符串str的unsafe.Pointer指針str會重新指向堆中內(nèi)存
   str += "world"
   //讓[]byte也指向堆中內(nèi)存
   byteArr := *(*[]byte)(unsafe.Pointer(&str))
   //修改
   byteArr[0] = 'w'
   fmt.Println(str)
}

[]byte和string的更高效轉(zhuǎn)換

一般情況下我們使用的強制類型的方式進行[]bytestring的互相轉(zhuǎn)換都會被替換為stringtoslicebyteslicebytetostring函數(shù), 這兩個函數(shù)都會新申請一個內(nèi)存空間, 然后將原本[]byte或string中的數(shù)據(jù)拷貝到新內(nèi)存空間中, 涉及一次內(nèi)存copy.

我們可以采用unsafe.Pointer當作一個中介來進行更高效的類型轉(zhuǎn)換, 事實上, 這個方式咱們之前已多次使用.

string->byte[]

func main(){
   str := "hello"
   //注意下面這一行, 是核心
   byteArr := *(*[]byte)(unsafe.Pointer(&str))
   fmt.Println(byteArr)
}

個人強烈不推薦這種寫法, 因為此時我們對byteArr的修改將導致超出預期的行為.

且因為stringStruct的數(shù)據(jù)結(jié)構(gòu)中只有unsafe.Pointer和一個int型變量len, 而切片的數(shù)據(jù)結(jié)構(gòu)slice則是有著unsafe.Pointer, int型變量len, 和int型變量cap, 所以我們通過上述方法把一個string強制轉(zhuǎn)換為一個[]byte時, 這個[]byte的cap將是一個完全不可控的值(取決于這部分內(nèi)存中的數(shù)據(jù), 且訪問這塊內(nèi)存本身就是非法的)

[]byte->string

func main(){
   //hello
   byteArr := []byte{104,101,108,108,111}
   str := *(*string)(unsafe.Pointer(&byteArr))
   fmt.Println(str)
}

相比起string->[]byte來說, []byte->string相對要安全很多, 我們只需要確保原始的[]byte不會被改變即可, 事實上, 這其實也是strings.Builder的實現(xiàn)原理之一

//string.Builder的String()函數(shù)本質(zhì)上就是把string.Builder中維護的[]byte轉(zhuǎn)換為string返回
func (b *Builder) String() string {
   return *(*string)(unsafe.Pointer(&b.buf))
}

結(jié)尾

我相信大家對字符串已經(jīng)有了一個比較不錯的認知了, 如果你之前是一名Java選手, 不要把字符串常量池等概念代入go中, 雖然Java和go中的字符串外在表現(xiàn)確實有些類似.

以上就是深入string理解Golang是怎樣實現(xiàn)的的詳細內(nèi)容,更多關(guān)于Golang string實現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Golang命令行進行debug調(diào)試操作

    Golang命令行進行debug調(diào)試操作

    今天小編就為大家分享一篇關(guān)于,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-04-04
  • Golang?channel為什么不會阻塞的原因詳解

    Golang?channel為什么不會阻塞的原因詳解

    這篇文章主要為大家介紹了Golang?channel為什么不會阻塞的原因詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-07-07
  • Go語言append切片添加元素的實現(xiàn)

    Go語言append切片添加元素的實現(xiàn)

    本文主要介紹了Go語言append切片添加元素的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-04-04
  • GO語言映射(Map)用法分析

    GO語言映射(Map)用法分析

    這篇文章主要介紹了GO語言映射(Map)用法,以實例形式較為詳細的分析了針對映射的創(chuàng)建、填充、遍歷及修改等操作的技巧,需要的朋友可以參考下
    2014-12-12
  • go?variant底層原理深入解析

    go?variant底層原理深入解析

    這篇文章主要為大家介紹了go?variant底層原理深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-11-11
  • 一文帶你全面掌握Go語言中的正則表達式

    一文帶你全面掌握Go語言中的正則表達式

    正則表達式是一種強大的模式匹配工具,能夠在文本中進行靈活的搜索和替換操作,本文將介紹?Golang?中的正則表達式語法,包括常用的匹配符號、模式修飾符以及示例應用,希望對大家有所幫助
    2023-05-05
  • golang解壓帶密碼的zip包的方法詳解

    golang解壓帶密碼的zip包的方法詳解

    ZIP 文件格式是一種常用的壓縮和歸檔格式,用于將多個文件和目錄打包到一個單獨的文件中,同時對其內(nèi)容進行壓縮以減少文件大小,golang zip包的解壓有官方的zip包(archive/zip),但是官方給的zip解壓包代碼只有解壓不帶密碼的zip包,下面給出解壓操作的封裝
    2024-07-07
  • GoLang實現(xiàn)Viper庫的封裝流程詳解

    GoLang實現(xiàn)Viper庫的封裝流程詳解

    Viper是一個用于Go語言應用程序的配置管理庫,它提供了一種簡單而靈活的方式來處理應用程序的配置,支持多種格式的配置文件,這篇文章主要介紹了GoLang封裝Viper庫的流程,感興趣的同學可以參考下文
    2023-05-05
  • 解決golang時間字符串轉(zhuǎn)time.Time的坑

    解決golang時間字符串轉(zhuǎn)time.Time的坑

    這篇文章主要介紹了解決golang時間字符串轉(zhuǎn)time.Time的坑,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Go1.18新特性使用Generics泛型進行流式處理

    Go1.18新特性使用Generics泛型進行流式處理

    這篇文章主要為大家介紹了Go1.18新特性使用Generics泛型進行流式處理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-06-06

最新評論

怀仁县| 克拉玛依市| 姚安县| 莱西市| 泗水县| 苏尼特右旗| 新巴尔虎左旗| 杭锦旗| 万安县| 班戈县| 乌拉特中旗| 大城县| 永清县| 页游| 虞城县| 佛冈县| 醴陵市| 龙南县| 湘潭县| 江源县| 华阴市| 扎囊县| 佛冈县| 西乌珠穆沁旗| 涪陵区| 庆云县| 武平县| 宁陵县| 衡水市| 古田县| 五大连池市| 托里县| 明溪县| 沅江市| 白河县| 永嘉县| 三亚市| 游戏| 肇东市| 剑阁县| 调兵山市|