這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)碛嘘P(guān)go語言中string轉(zhuǎn)為[]byte會(huì)遇到的問題,以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
創(chuàng)新互聯(lián)技術(shù)團(tuán)隊(duì)十年來致力于為客戶提供成都網(wǎng)站制作、成都做網(wǎng)站、成都品牌網(wǎng)站建設(shè)、網(wǎng)絡(luò)營(yíng)銷推廣、搜索引擎SEO優(yōu)化等服務(wù)。經(jīng)過多年發(fā)展,公司擁有經(jīng)驗(yàn)豐富的技術(shù)團(tuán)隊(duì),先后服務(wù)、推廣了成百上千網(wǎng)站,包括各類中小企業(yè)、企事單位、高校等機(jī)構(gòu)單位。1. 背景
package main import "fmt" func main() { s := []byte("") s1 := append(s, 'a') s2 := append(s, 'b') //fmt.Println(s1, "==========", s2) fmt.Println(string(s1), "==========", string(s2)) } // 出現(xiàn)個(gè)讓我理解不了的現(xiàn)象, 注釋時(shí)候輸出是 b ========== b // 取消注釋輸出是 [97] ========== [98] a ========== b
2. slice
2.1 內(nèi)部結(jié)構(gòu)
先拋去注釋的這行代碼//fmt.Println(s1, "==========", s2),后面在講。 當(dāng)輸出 b ========== b時(shí),已經(jīng)不符合預(yù)期結(jié)果a和b了。我們知道slice內(nèi)部并不會(huì)存儲(chǔ)真實(shí)的值,而是對(duì)數(shù)組片段的引用,其內(nèi)部結(jié)構(gòu)是:
type slice struct { data uintptr len int cap int}
其中data是指向數(shù)組元素的指針,len是指slice要引用數(shù)組中的元素?cái)?shù)量。cap是指要引用數(shù)組中(從data指向開始計(jì)算)剩余的元素?cái)?shù)量,這個(gè)數(shù)量減去len,就是還能向這個(gè)slice(數(shù)組)添加多少元素,如果超出就會(huì)發(fā)生數(shù)據(jù)的復(fù)制。slice的示意圖:
s := make([]byte, 5)// 下圖
s = s[2:4] //會(huì)重新生成新的slice,并賦值給s。與底層數(shù)組的引用也發(fā)生了改變
2.2 覆蓋前值
回到問題上,由此可以推斷出:s := []byte("") 這行代碼中的s實(shí)際引用了一個(gè) byte 的數(shù)組。
其capacity 是32,length是 0:
s := []byte("") fmt.Println(cap(s), len(s)) //輸出: 32 0
關(guān)鍵點(diǎn)在于下面代碼s1 := append(s, 'a')中的append,并沒有在原slice修改,當(dāng)然也沒辦法修改,因?yàn)樵贕o中都是值傳遞的。當(dāng)把s傳入append函數(shù)內(nèi)時(shí),已經(jīng)復(fù)制出一份s1,然后在s1上追加 a,s1長(zhǎng)度是增加了1,但s長(zhǎng)度仍然是0:
s := []byte("") fmt.Println(cap(s), len(s)) s1 := append(s, 'a') fmt.Println(cap(s1), len(s1)) // 輸出 // 32 0 // 32 1
由于s,s1指向同一份數(shù)組,所以在s1上進(jìn)行append a操作時(shí)(底層數(shù)組[0]=a),也是s所指向數(shù)組的操作,但s本身不會(huì)有任何變化。這也是Go中append的寫法都是:
s = append(s,'a')
append函數(shù)會(huì)返回s1,需要重新賦值給s。 如果不賦值的話,s本身記錄的數(shù)據(jù)就滯后了,再次對(duì)其append,就會(huì)從滯后的數(shù)據(jù)開始操作。雖然看起是append,實(shí)際上確是把上一次append的值給覆蓋了。
所以問題的答案是:后append的b,把上次append的a給覆蓋了,所以才會(huì)輸出b b。
假設(shè)底層數(shù)組是arr,如注釋:
s := []byte("") s1 := append(s, 'a') // 等同于 arr[0] = 'a' s2 := append(s, 'b') // 等同于 arr[0] = 'b' fmt.Println(string(s1), "==========", string(s2)) // 只是把同一份數(shù)組打印出來了
3. string
3.1 重新分配
老濕,能不能再給力一點(diǎn)?可以,我們繼續(xù),先來看個(gè)題:
s := []byte{} s1 := append(s, 'a') s2 := append(s, 'b') fmt.Println(string(s1), ",", string(s2)) fmt.Println(cap(s), len(s))
猜猜輸出什么?
答案是:a , b 和 0 0,符合預(yù)期。
上面2.2章節(jié)例子中輸出的是:32,0。看來問題關(guān)鍵在這里,兩者差別在于一個(gè)是默認(rèn)[]byte{},另外個(gè)是空字符串轉(zhuǎn)的[]byte("")。其長(zhǎng)度都是0,比較好理解,但為什么容量是32就不符合預(yù)期輸出了?
因?yàn)?capacity 是數(shù)組還能添加多少的容量,在能滿足的情況,不會(huì)重新分配。所以 capacity-length=32,是足夠appenda,b的。我們用make來驗(yàn)證下:
// append 內(nèi)會(huì)重新分配,輸出a,b s := make([]byte, 0, 0) // append 內(nèi)不會(huì)重新分配,輸出b,b,因?yàn)槿萘繛?,足夠append s := make([]byte, 0, 1) s1 := append(s, 'a') s2 := append(s, 'b') fmt.Println(string(s1), ",", string(s2))
重新分配指的是:append 會(huì)檢查slice大小,如果容量不夠,會(huì)重新創(chuàng)建個(gè)更大的slice,并把原數(shù)組復(fù)制一份出來。在make([]byte,0,0)這樣情況下,s容量肯定不夠用,所以s1,s2使用的都是各自從s復(fù)制出來的數(shù)組,結(jié)果也自然符合預(yù)期a,b了。
測(cè)試重新分配后的容量變大,打印s1:
s := make([]byte, 0, 0) s1 := append(s, 'a') fmt.Println(cap(s1), len(s1)) // 輸出 8,1。重新分配后擴(kuò)大了
3.2 二者轉(zhuǎn)換
那為什么空字符串轉(zhuǎn)的slice的容量是32?而不是0或者8呢?
只好祭出殺手锏了,翻源碼。Go官方提供的工具,可以查到編譯后調(diào)用的匯編信息,不然在大片源碼中搜索也很累。
-gcflags 是傳遞參數(shù)給Go編譯器,-S -S是打印匯編調(diào)用信息和數(shù)據(jù),-S只打印調(diào)用信息。
go run -gcflags '-S -S' main.go
下面是輸出:
0x0000 00000 () TEXT "".main(SB), $264-0 0x003e 00062 () MOVQ AX, (SP) 0x0042 00066 () XORPS X0, X0 0x0045 00069 () MOVUPS X0, 8(SP) 0x004a 00074 () PCDATA $0, $0 0x004a 00074 () CALL runtime.stringtoslicebyte(SB) 0x004f 00079 () MOVQ 32(SP), AX b , b
Go使用的是plan9匯編語法,雖然整體有些不好理解,但也能看出我們需要的關(guān)鍵點(diǎn):
CALL runtime.stringtoslicebyte(SB)
定位源碼到src\runtime\string.go:
從stringtoslicebyte函數(shù)中可以看出容量32的源頭,見注釋:
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{} // tmpBuf的默認(rèn)容量是32 b = buf[:len(s)] // 創(chuàng)建個(gè)容量為32,長(zhǎng)度為0的新slice,賦值給b。 } else { b = rawbyteslice(len(s)) } copy(b, s) // s是空字符串,復(fù)制過去也是長(zhǎng)度0 return b }
那為什么不是走else中rawbyteslice函數(shù)?
func rawbyteslice(size int) (b []byte) { cap := roundupsize(uintptr(size)) p := mallocgc(cap, nil, false) if cap != uintptr(size) { memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size)) } *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)} return }
如果走else的話,容量就不是32了。假如走的話,也不影響得出的結(jié)論(覆蓋),可以測(cè)試下:
s := []byte(strings.Repeat("c", 33)) s1 := append(s, 'a') s2 := append(s, 'b') fmt.Println(string(s1), ",", string(s2)) // cccccccccccccccccccccccccccccccccb , cccccccccccccccccccccccccccccccccb
4. 逃逸分析
老濕,能不能再給力一點(diǎn)?什么時(shí)候該走else?老濕你說了大半天,坑還沒填,為啥加上注釋就符合預(yù)期輸出a,b? 還有加上注釋為啥連容量都變了?
s := []byte("") fmt.Println(cap(s), len(s)) s1 := append(s, 'a') s2 := append(s, 'b') fmt.Println(s1, ",", s2) fmt.Println(string(s1), ",", string(s2)) //輸出 // 0 0 // [97] ========== [98] // a , b
如果用逃逸分析來解釋的話,就比較好理解了,先看看什么是逃逸分析。
4.1 逃到堆上
4.2 提高性能
如果一個(gè)函數(shù)或子程序內(nèi)有局部對(duì)象,返回時(shí)返回該對(duì)象的指針,那這個(gè)指針可能在任何其他地方會(huì)被引用,就可以說該指針就成功“逃逸”了 。 而逃逸分析(escape analysis)就是分析這類指針范圍的方法,這樣做的好處是提高性能:
大的好處應(yīng)該是減少gc的壓力,不逃逸的對(duì)象分配在棧上,當(dāng)函數(shù)返回時(shí)就回收了資源,不需要gc標(biāo)記清除。
因?yàn)樘右莘治鐾旰罂梢源_定哪些變量可以分配在棧上,棧的分配比堆快,性能好
同步消除,如果定義的對(duì)象的方法上有同步鎖,但在運(yùn)行時(shí),卻只有一個(gè)線程在訪問,此時(shí)逃逸分析后的機(jī)器碼,會(huì)去掉同步鎖運(yùn)行。
Go在編譯的時(shí)候進(jìn)行逃逸分析,來決定一個(gè)對(duì)象放棧上還是放堆上,不逃逸的對(duì)象放棧上,可能逃逸的放堆上 。(推薦:go視頻教程)
go tool compile -m main.go
取消注釋fmt.Println(s1, ",", s2) 后 ([]byte)("")會(huì)逃逸到堆上:
main.go:23:13: s1 escapes to heap main.go:20:13: ([]byte)("") escapes to heap // 逃逸到堆上 main.go:23:18: "," escapes to heap main.go:23:18: s2 escapes to heap main.go:24:20: string(s1) escapes to heap main.go:24:20: string(s1) escapes to heap main.go:24:26: "," escapes to heap main.go:24:37: string(s2) escapes to heap main.go:24:37: string(s2) escapes to heap main.go:23:13: main ... argument does not escape main.go:24:13: main ... argument does not escape
加上注釋//fmt.Println(s1, ",", s2)不會(huì)逃逸到堆上:
go tool compile -m main.go main.go:24:20: string(s1) escapes to heap main.go:24:20: string(s1) escapes to heap main.go:24:26: "," escapes to heap main.go:24:37: string(s2) escapes to heap main.go:24:37: string(s2) escapes to heap main.go:20:13: main ([]byte)("") does not escape //不逃逸 main.go:24:13: main ... argument does not escape
4.3 逃逸分配
接著繼續(xù)定位調(diào)用stringtoslicebyte的地方,在src\cmd\compile\internal\gc\walk.go 文件。 為了便于理解,下面代碼進(jìn)行了匯總:
const ( EscUnknown = iota EscNone // 結(jié)果或參數(shù)不逃逸堆上. ) case OSTRARRAYBYTE: a := nodnil() //默認(rèn)數(shù)組為空 if n.Esc == EscNone { // 在棧上為slice創(chuàng)建臨時(shí)數(shù)組 t := types.NewArray(types.Types[TUINT8], tmpstringbufsize) a = nod(OADDR, temp(t), nil) } n = mkcall("stringtoslicebyte", n.Type, init, a, conv(n.Left, types.Types[TSTRING]))
不逃逸情況下會(huì)分配個(gè)32字節(jié)的數(shù)組 t。逃逸情況下不分配,數(shù)組設(shè)置為 nil,所以s的容量是0。接著從s上append a,b到s1,s2,其必然會(huì)發(fā)生復(fù)制,所以不會(huì)發(fā)生覆蓋前值,也符合預(yù)期結(jié)果a,b 。再看stringtoslicebyte就很清晰了。
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 }
4.4 大小分配
不逃逸情況下默認(rèn)32。那逃逸情況下分配策略是?
s := []byte("a") fmt.Println(cap(s)) s1 := append(s, 'a') s2 := append(s, 'b') fmt.Print(s1, s2)
如果是空字符串它的輸出:0?!盿“字符串時(shí)輸出:8。
大小取決于src\runtime\size.go 中的roundupsize 函數(shù)和 class_to_size 變量。
這些增加大小的變化,是由 src\runtime\mksizeclasses.go生成的。
5. 版本差異
老濕,能不能再給力一點(diǎn)? 老濕你講的全是錯(cuò)誤的,我跑的結(jié)果和你是反的。對(duì),你沒錯(cuò),作者也沒錯(cuò),畢竟我們?cè)谟肎o寫程序,如果Go底層發(fā)生變化了,肯定結(jié)果不一樣。作者在調(diào)研過程中,發(fā)現(xiàn)另外博客得到的stringtoslicebyte源碼是:
func stringtoslicebyte(s String) (b Slice) { b.array = runtime·mallocgc(s.len, 0, FlagNoScan|FlagNoZero); b.len = s.len; b.cap = s.len; runtime·memmove(b.array, s.str, s.len); }
上面版本的源碼,得到的結(jié)果,也是符合預(yù)期的,因?yàn)椴粫?huì)默認(rèn)分配32字節(jié)的數(shù)組。
繼續(xù)翻舊版代碼,到1.3.2版是這樣:
func stringtoslicebyte(s String) (b Slice) { uintptr cap; cap = runtime·roundupsize(s.len); b.array = runtime·mallocgc(cap, 0, FlagNoScan|FlagNoZero); b.len = s.len; b.cap = cap; runtime·memmove(b.array, s.str, s.len); if(cap != b.len) runtime·memclr(b.array+b.len, cap-b.len); }
1.6.4版:
func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { b = buf[:len(s):len(s)] } else { b = rawbyteslice(len(s)) } copy(b, s) return b }
更古老的:
struct __go_open_array __go_string_to_byte_array (String str) { uintptr cap; unsigned char *data; struct __go_open_array ret; cap = runtime_roundupsize (str.len); data = (unsigned char *) runtime_mallocgc (cap, 0, FlagNoScan | FlagNoZero); __builtin_memcpy (data, str.str, str.len); if (cap != (uintptr) str.len) __builtin_memset (data + str.len, 0, cap - (uintptr) str.len); ret.__values = (void *) data; ret.__count = str.len; ret.__capacity = str.len; return ret; }
總結(jié)下:
注釋時(shí)輸出b,b。是因?yàn)闆]有逃逸,所以分配了默認(rèn)32字節(jié)大小的數(shù)組,2次append都是在數(shù)組[0]賦值,后值覆蓋前值,所以才是b,b。
消注釋時(shí)輸出a,b。是因?yàn)閒mt.Println引用了s,逃逸分析時(shí)發(fā)現(xiàn)需要逃逸并且是空字符串,所以分配了空數(shù)組。2次append都是操作各自重新分配后的新slice,所以輸出a,b。
上述就是小編為大家分享的go語言中string轉(zhuǎn)為[]byte會(huì)遇到的問題了,如果您也有類似的疑惑,不妨參照上述方法進(jìn)行嘗試。如果想了解更多相關(guān)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊。