深入string理解Golang是怎樣實(shí)現(xià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)行[]byte
和string
的互相轉(zhuǎn)換都會(huì)被替換為stringtoslicebyte
和slicebytetostring
函數(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)文章
如何控制Go編碼JSON數(shù)據(jù)時(shí)的行為(問題及解決方案)
今天來聊一下我在Go中對(duì)數(shù)據(jù)進(jìn)行 JSON 編碼時(shí)遇到次數(shù)最多的三個(gè)問題以及解決方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-02-02

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

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

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