Go語(yǔ)言中slice作為參數(shù)傳遞時(shí)遇到的一些“坑”
前言
相信看到這個(gè)題目,可能大家都覺(jué)得是一個(gè)老生常談的月經(jīng)topic了。一直以來(lái)其實(shí)把握一個(gè)“值傳遞”基本上就能理解各種情況了,不過(guò)最近遇到了更深一點(diǎn)的“小坑”,與大家分享一下。
首先還是從最簡(jiǎn)單的說(shuō)起,看下面代碼:
func main() { a := []int{7,8,9} fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) ap(a) fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) } func ap(a []int) { a = append(a, 10) }
以上代碼的輸出是什么呢?
我這里不賣(mài)關(guān)子了直接說(shuō),再調(diào)用ap函數(shù)進(jìn)行append操作后,a依然是[]int{7,8,9}。原因很簡(jiǎn)單,Go中沒(méi)有引用傳遞全是值傳遞,值傳遞意味著傳遞的是數(shù)據(jù)的拷貝。這句話(huà)新手可能稍微有點(diǎn)云里霧里,而實(shí)際情況又比較詭異,比如說(shuō)下面代碼:
func main() { a := []int{7,8,9} fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) ap(a) fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) } func ap(a []int) { a[0] = 1 a = append(a, 10) }
這時(shí)ap后再輸出a,會(huì)看到a[0]變成了1,但a的cap依然是3,看起來(lái)10并沒(méi)有被append進(jìn)去?
這看起來(lái)就比較匪夷所思了,不是說(shuō)值傳遞嗎,為什么還是影響外部變量的值了呢?按理說(shuō)要么都變要么都不變才說(shuō)得過(guò)去啊。
這實(shí)際上并不是匪夷所思,因?yàn)镚o和C不一樣,slice看起來(lái)像數(shù)組,實(shí)際上是一個(gè)結(jié)構(gòu)體,在源碼中的數(shù)據(jù)結(jié)構(gòu)是:
type slice struct { array unsafe.Pointer len int cap int }
這個(gè)結(jié)構(gòu)體其實(shí)也很好理解,array是一個(gè)真正的數(shù)組指針,指向一段連續(xù)內(nèi)存空間的頭部,len和cap代表長(zhǎng)度和容量。
換句話(huà)說(shuō),你看起來(lái)在代碼里傳參時(shí)寫(xiě)的是ap(a []int),實(shí)際上在代碼編譯期,這段代碼變成了ap(a runtime.slice)
你可以嘗試這么理解,把a(bǔ)p(a)替換成ap(array: 0x123, len: 3, cap: 3)
。可以很明顯的看到,傳遞到ap函數(shù)的三個(gè)參數(shù),僅僅是3個(gè)數(shù)值,并沒(méi)有和外部變量a建立任何引用關(guān)系。這便是值傳遞。
但是,你可能會(huì)疑惑,為什么我改了a[0]的值,也會(huì)在外面體現(xiàn)呢?其實(shí)看到這里你應(yīng)該已經(jīng)可以自己想明白了,因?yàn)閍rray是一個(gè)地址值(比如0x123),這個(gè)地址傳入了ap函數(shù),但是它代表的地址0x123和外部a的0x123是一個(gè)內(nèi)存地址,這時(shí)候你修改a[0],實(shí)際上是修改0x123地址中存放的值,所以外部當(dāng)然會(huì)受影響了。
舉個(gè)形象點(diǎn)的例子,假設(shè)你是火車(chē)站貨物管理員,你管理的是第1到第3節(jié)車(chē)廂(車(chē)廂是互通的)的裝卸貨貨。有一天你生病了,找個(gè)人(叫A)臨時(shí)來(lái)接手一下。但是火車(chē)的貨不是誰(shuí)想碰就碰的,你得有證明才行。于是你把你手上的證明原件復(fù)印了一份給A,同時(shí)把第一節(jié)車(chē)廂的鑰匙給A。由于剛好那幾天比較忙,站長(zhǎng)又讓A也負(fù)責(zé)第四節(jié)車(chē)廂,于是A也得到了車(chē)廂4的證明原件。一段時(shí)間后,你生病回來(lái),你依然只有1到3節(jié)車(chē)廂的證件,你可以看到最近A在1到3車(chē)廂搞的事情,但是你沒(méi)有資格去4車(chē)廂。
以上例子應(yīng)該可以很好的說(shuō)明slice傳參的場(chǎng)景,記住,Go中只有值傳遞。
是不是就完事兒了呢?然而事情并沒(méi)有這么簡(jiǎn)單。最近我工作時(shí)就遇到這個(gè)問(wèn)題了。按照上面的舉例,雖然你沒(méi)有資格去查看4車(chē)廂,但是如果你好奇,你可以偷看啊,因?yàn)樗鼈兪沁B續(xù)的互通的,正如數(shù)組也是一段連續(xù)的內(nèi)存,于是就有這樣的代碼:
func main() { a := []int{} a = append(a, 7,8,9) fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) ap(a) fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) p := unsafe.Pointer(&a[2]) q := uintptr(p)+8 t := (*int)(unsafe.Pointer(q)) fmt.Println(*t) } func ap(a []int) { a = append(a, 10) }
雖然外部的cap和len并沒(méi)有改變,但是ap函數(shù)往同一段內(nèi)存地址append了一個(gè)10,那我是不是可以用比較trick的方法去偷看呢?比如找到a[2]
的地址,往后挪一個(gè)int的長(zhǎng)度,就應(yīng)該是ap函數(shù)新增的10了吧?這里需要注意,Go官網(wǎng)的server是32位的,所以在go playground執(zhí)行這段代碼時(shí),int是4字節(jié)。
執(zhí)行結(jié)果和我預(yù)想的一樣!
但是問(wèn)題接踵而至
func main() { a := []int{7,8,9} fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) ap(a) fmt.Printf("len: %d cap:%d data:%+v\n", len(a), cap(a), a) p := unsafe.Pointer(&a[2]) q := uintptr(p)+8 t := (*int)(unsafe.Pointer(q)) fmt.Println(*t) } func ap(a []int) { a = append(a, 10) }
這和上面一個(gè)例子唯一的區(qū)別就是slice一開(kāi)始是用[]int{7,8,9}
這種方式初始化。執(zhí)行結(jié)果*t是3而不是10,這就比較困惑了。為啥?不是一段連續(xù)的內(nèi)存空間嗎?
這里其實(shí)涉及到的問(wèn)題是slice的growth問(wèn)題,當(dāng)append時(shí)發(fā)現(xiàn)cap不夠了,會(huì)重新分配空間,具體源碼參見(jiàn) runtime/slice.go中的growslice函數(shù)。我這里就不講太多細(xì)節(jié),只講結(jié)果。當(dāng)發(fā)生growslice時(shí),會(huì)給slice重新分配一段更大的內(nèi)存,然后把原來(lái)的數(shù)據(jù)copy過(guò)去,把slice的array指針指向新內(nèi)存。也就是說(shuō),假如之前的數(shù)據(jù)是存放到內(nèi)存地址 0x0 0x8 0x10,當(dāng)不發(fā)生growslice,新append的數(shù)值會(huì)存到0x18,然而當(dāng)發(fā)生growslice,以前的所有數(shù)據(jù)被copy到新的地址0x1000 0x1008 0x1010,新append的值放到0x1018了。
這時(shí)候你就可以理解為什么有時(shí)候用unsafe能拿到數(shù)據(jù),有時(shí)候拿不到了。或許你可以理解為什么這個(gè)包叫做unsafe了。不過(guò)unsafe不是真的unsafe,是說(shuō)如果你使用的姿勢(shì)不對(duì)就非常容易u(yù)nsafe。但是如果姿勢(shì)優(yōu)雅,其實(shí)很safe。對(duì)于slice操作,如果要使用unsafe,千萬(wàn)記得關(guān)注cap是否發(fā)送變化,它意味著內(nèi)存的遷移
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- 理解Golang中的數(shù)組(array)、切片(slice)和map
- Go語(yǔ)言中的Slice學(xué)習(xí)總結(jié)
- Go語(yǔ)言入門(mén)教程之Arrays、Slices、Maps、Range操作簡(jiǎn)明總結(jié)
- Go語(yǔ)言中slice的用法實(shí)例分析
- Golang學(xué)習(xí)筆記(四):array、slice、map
- 深入解析Go語(yǔ)言編程中slice切片結(jié)構(gòu)
- 深入理解golang的基本類(lèi)型排序與slice排序
- 詳解Go中Map類(lèi)型和Slice類(lèi)型的傳遞
- 淺談golang slice 切片原理
- Golang slice切片操作之切片的追加、刪除、插入等
- Go基礎(chǔ)Slice教程詳解
- golang中range在slice和map遍歷中的注意事項(xiàng)
- Go 中 slice 的 In 功能實(shí)現(xiàn)探索
- Golang中的Slice與數(shù)組及區(qū)別詳解
- golang語(yǔ)言如何將interface轉(zhuǎn)為int, string,slice,struct等類(lèi)型
- go 判斷兩個(gè) slice/struct/map 是否相等的實(shí)例
- Go基礎(chǔ)系列:Go切片(分片)slice詳解
相關(guān)文章
go語(yǔ)言中使用ent做關(guān)聯(lián)查詢(xún)的示例詳解
go語(yǔ)言的ent框架是facebook開(kāi)源的ORM框架,是go語(yǔ)言開(kāi)發(fā)中的常用框架,而關(guān)聯(lián)查詢(xún)又是日常開(kāi)發(fā)中的常見(jiàn)數(shù)據(jù)庫(kù)操作,故文本給出一個(gè)使用ent做關(guān)聯(lián)查詢(xún)的使用示例,需要的朋友可以參考下2024-02-02go語(yǔ)言阻塞函數(shù)和非阻塞函數(shù)實(shí)現(xiàn)
本文主要介紹了go語(yǔ)言阻塞函數(shù)和非阻塞函數(shù)實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03Go項(xiàng)目實(shí)現(xiàn)優(yōu)雅關(guān)機(jī)與平滑重啟功能
無(wú)論是優(yōu)雅關(guān)機(jī)還是優(yōu)雅重啟歸根結(jié)底都是通過(guò)監(jiān)聽(tīng)特定系統(tǒng)信號(hào),然后執(zhí)行一定的邏輯處理保障當(dāng)前系統(tǒng)正在處理的請(qǐng)求被正常處理后再關(guān)閉當(dāng)前進(jìn)程,這篇文章主要介紹了Go實(shí)現(xiàn)優(yōu)雅關(guān)機(jī)與平滑重啟 ,需要的朋友可以參考下2022-10-10Go中函數(shù)的使用細(xì)節(jié)與注意事項(xiàng)詳解
在Go語(yǔ)言中函數(shù)可是一等的(first-class)公民,函數(shù)類(lèi)型也是一等的數(shù)據(jù)類(lèi)型,下面這篇文章主要給大家介紹了關(guān)于Go中函數(shù)的使用細(xì)節(jié)與注意事項(xiàng)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11Go語(yǔ)言sync.Map詳解及使用場(chǎng)景
Go語(yǔ)言中的sync.Map是一個(gè)高效的并發(fā)安全映射結(jié)構(gòu),適用于高并發(fā)讀多寫(xiě)少的場(chǎng)景,它通過(guò)讀寫(xiě)分離、無(wú)鎖讀取路徑、寫(xiě)入時(shí)的鎖保護(hù)等機(jī)制,提高了讀取性能并減少了鎖競(jìng)爭(zhēng),sync.Map不需要手動(dòng)管理鎖,降低了編程復(fù)雜性,適合需要簡(jiǎn)單并發(fā)訪問(wèn)的場(chǎng)合2024-10-10