欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

引言

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

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

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

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

內(nèi)容介紹

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

字符串?dāng)?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: 這個(gè)字符串的長(zhǎng)度, int類型, 在64bit機(jī)上大小8字節(jié), 在32bit機(jī)上大小4字節(jié).

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

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

我們把字符串分為兩種

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

編譯期即可確定的字符串

a := "hello world"

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

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

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

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

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

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

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

我們可以做出如下驗(yàn)證方式

//因?yàn)閟tringStruct是runtime包下一個(gè)不對(duì)外暴露的數(shù)據(jù)結(jié)構(gòu),
//所以我們新建一個(gè)結(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ǔ)/b對(duì)應(yīng)的二進(jìn)制數(shù)據(jù)重新解析為Reception,
   //而Reception和stringStruct的結(jié)構(gòu)相同, 所以不會(huì)出問題.
   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兩個(gè)stringStruct被分配到不同地址, 而他們的str則指向了同一地址.

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

字面量是否會(huì)在編譯器合并

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

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

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

main.go:8:13: inlining call to fmt.Println
//如果合并的話, 應(yīng)該是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版本是否會(huì)合并.

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

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

當(dāng)我們用+連接多個(gè)字符串時(shí), 會(huì)發(fā)生什么

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

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

分配到棧上還是堆上

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

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

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

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

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

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

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

驗(yàn)證

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

//因?yàn)閟tringStruct是runtime包下一個(gè)不對(duì)外暴露的數(shù)據(jù)結(jié)構(gòu),
//所以我們新建一個(gè)結(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ǔ)/b對(duì)應(yīng)的二進(jìn)制數(shù)據(jù)重新解析為Reception,
   //而Reception和stringStruct的結(jié)構(gòu)相同, 所以不會(huì)出問題.
   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真的是一個(gè)十分有趣的函數(shù), 因此我決定對(duì)他進(jìn)行詳細(xì)的分析, 但他相對(duì)有點(diǎn)難度, 如果靜下心來讀懂, 定能讓您有所收獲. 我們直接上源碼逐行分析

func rawstring(size int) (s string, b []byte) {
   //在堆中申請(qǐng)內(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ù)十分簡(jiǎn)單, 因?yàn)閟tring和stringStruct的結(jié)構(gòu)完全相同, 因此他直接通過把(*stringStruct)(unsafe.Pointer(sp))來把字符串指針sp轉(zhuǎn)換為stringStruct指針, 然后通過stringStruct指針來獲取stringStruct結(jié)構(gòu)體.

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

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

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

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

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

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

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

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

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

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

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

而對(duì)于運(yùn)行時(shí)通過+拼接得到的新串, 修改堆棧中存放的字面量則可以成功

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

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

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

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

string->byte[]

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

個(gè)人強(qiáng)烈不推薦這種寫法, 因?yàn)榇藭r(shí)我們對(duì)byteArr的修改將導(dǎo)致超出預(yù)期的行為.

且因?yàn)閟tringStruct的數(shù)據(jù)結(jié)構(gòu)中只有unsafe.Pointer和一個(gè)int型變量len, 而切片的數(shù)據(jù)結(jié)構(gòu)slice則是有著unsafe.Pointer, int型變量len, 和int型變量cap, 所以我們通過上述方法把一個(gè)string強(qiáng)制轉(zhuǎn)換為一個(gè)[]byte時(shí), 這個(gè)[]byte的cap將是一個(gè)完全不可控的值(取決于這部分內(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相對(duì)要安全很多, 我們只需要確保原始的[]byte不會(huì)被改變即可, 事實(shí)上, 這其實(shí)也是strings.Builder的實(shí)現(xiàn)原理之一

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

結(jié)尾

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

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

相關(guān)文章

  • 試了下Golang實(shí)現(xiàn)try catch的方法

    試了下Golang實(shí)現(xiàn)try catch的方法

    雖然在使用Golang的時(shí)候發(fā)現(xiàn)沒有try catch這種錯(cuò)誤處理機(jī)制但是想一想golang作為一門優(yōu)雅的語言,似乎也是情理之中。那么夠怎么捕獲異常呢,本文就來介紹一下
    2021-07-07
  • Golang的strings.Split()踩坑記錄

    Golang的strings.Split()踩坑記錄

    工作中,當(dāng)我們需要對(duì)字符串按照某個(gè)字符串切分成字符串?dāng)?shù)組數(shù)時(shí),常用到strings.Split(),本文主要介紹了Golang的strings.Split()踩坑記錄,感興趣的可以了解一下
    2022-05-05
  • 詳解Go語言中如何高效遍歷目錄

    詳解Go語言中如何高效遍歷目錄

    目錄遍歷是一個(gè)很常見的操作,它的使用場(chǎng)景有如文件目錄查看、文件系統(tǒng)清理、日志分析、項(xiàng)目構(gòu)建等,本文將詳細(xì)介紹在Go中幾種遍歷目錄文件的方法,需要的可以參考下
    2024-02-02
  • Go語言pointer及switch?fallthrough實(shí)戰(zhàn)詳解

    Go語言pointer及switch?fallthrough實(shí)戰(zhàn)詳解

    這篇文章主要為大家介紹了Go語言pointer及switch?fallthrough實(shí)戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • 重學(xué)Go語言之文件操作詳解

    重學(xué)Go語言之文件操作詳解

    有很多場(chǎng)景都需要對(duì)文件進(jìn)行讀取或者寫入,比如讀取配置文件或者寫入日志文件,在Go語言中,操作文件應(yīng)該算是一件比較簡(jiǎn)單的事情,我們?cè)谶@一篇文章中,一起來探究一下
    2023-08-08
  • golang協(xié)程與線程區(qū)別簡(jiǎn)要介紹

    golang協(xié)程與線程區(qū)別簡(jiǎn)要介紹

    這篇文章主要介紹了golang協(xié)程與線程區(qū)別簡(jiǎn)要介紹,進(jìn)程是操作系統(tǒng)資源分配的基本單位,是程序運(yùn)行的實(shí)例,線程是操作系統(tǒng)調(diào)度到CPU中執(zhí)行的基本單位
    2022-06-06
  • 在Go語言程序中使用gojson來解析JSON格式文件

    在Go語言程序中使用gojson來解析JSON格式文件

    這篇文章主要介紹了在Go語言程序中使用gojson來解析JSON格式文件的方法,Go是由Google開發(fā)的高人氣新興編程語言,需要的朋友可以參考下
    2015-10-10
  • 最新評(píng)論