新聞中心
最近跟同事做項(xiàng)目,由于要在函數(shù)里向一個(gè) Map 中寫(xiě)入不少數(shù)據(jù),這個(gè) Map 是作為參數(shù)傳到函數(shù)里的。他問(wèn)了我一個(gè)問(wèn)題: “如果把 Map 作為函數(shù)參數(shù)傳遞,會(huì)不會(huì)像用 Slice 做參數(shù)時(shí)一樣詭異,是不是一定要把 Map 當(dāng)成返回值返回才能讓函數(shù)外部的 Map 變量看到這里添加的數(shù)據(jù)”?

創(chuàng)新互聯(lián)建站主要從事網(wǎng)站設(shè)計(jì)制作、成都網(wǎng)站制作、網(wǎng)頁(yè)設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)耀州,10余年網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專(zhuān)業(yè),歡迎來(lái)電咨詢建站服務(wù):028-86922220
啥叫會(huì)不會(huì)像用 Slice 做參數(shù)時(shí)一樣詭異?同事沒(méi)有明說(shuō),其實(shí)我已經(jīng)猜到他說(shuō)的是什么意思了,說(shuō)的應(yīng)該是 Slice 的底層數(shù)組如果發(fā)生了擴(kuò)容后會(huì)讓函數(shù)內(nèi)外原本指向同一個(gè)底層數(shù)組的兩個(gè) Slice 變量,分別指向兩個(gè)不同的底層數(shù)組。
最后就導(dǎo)致了函數(shù)內(nèi)做的數(shù)據(jù)添加,但是函數(shù)外原來(lái)的 Slice 變量并沒(méi)有任何改變的詭異效果。光看字兒解釋起來(lái)有點(diǎn)難懂,舉個(gè)例子,有下面這樣一個(gè)程序。
func main() {
s := []int{1, 2, 3}
reverse(s)
fmt.Println(s)
}
func reverse(s []int) {
s = append(s, 999, 1000, 1001)
for i, j := 0, len(s)-1; i < j; i++ {
j = len(s) - (i + 1)
s[i], s[j] = s[j], s[i]
}
}
本來(lái)切片只有 3 個(gè)元素,分別是 1,2,3。我們把切片賦給了變量 s,然后用變量 s 作為參數(shù)傳給了函數(shù) reverse 進(jìn)行處理,函數(shù) reverse 在反轉(zhuǎn)切片元素之前還給原來(lái)的切片先追加了幾個(gè)值,這就導(dǎo)致了切片發(fā)生擴(kuò)容。因?yàn)榍衅瑢?shí)際上并不是一個(gè)指針類(lèi)型,它的運(yùn)行時(shí)類(lèi)型表示是 SliceHeader。
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
因?yàn)?Go 里邊有一切都是值傳遞的規(guī)則,所以切片作為參數(shù)時(shí),會(huì)在函數(shù)內(nèi)重新拷貝一個(gè) SliceHeader 結(jié)構(gòu)體,只不過(guò)結(jié)構(gòu)體的 Data 指針一開(kāi)始跟外部切片的指向是一樣的,都是同一個(gè)底層數(shù)據(jù)。
這就導(dǎo)致了函數(shù)內(nèi)切片 SliceHeader 里的 Data 指針發(fā)生變化后,函數(shù)外原來(lái)的切片還是指向原來(lái)的底層數(shù)組。最后結(jié)果,打印函數(shù)外切片變量輸出的是 [1, 2, 3],但函數(shù)里邊的切片已經(jīng)是 [1001, 1000, 999, 3, 2, 1] 了。
下面這個(gè)圖,展示了這個(gè)函數(shù)內(nèi)外切片指向的底層數(shù)組發(fā)生變化的過(guò)程。
那么如果用 Map 當(dāng)函數(shù)參數(shù)時(shí),有這檔子破事兒?jiǎn)?誒,提到這我就要吐槽下這個(gè)一切都是傳值的設(shè)計(jì)了,把一些寫(xiě) Go 的程序員搞的戰(zhàn)戰(zhàn)兢兢,用 Map 和結(jié)構(gòu)體指針當(dāng)參數(shù)的時(shí)候也老琢磨底層會(huì)不會(huì)變。
當(dāng)然我也不是寫(xiě) Go 的時(shí)候都盲目自信,一般書(shū)上、別人文章里寫(xiě)的東西我在用的時(shí)候,如果不確定他們說(shuō)的對(duì)不對(duì),我都會(huì)寫(xiě)個(gè)單測(cè)試一試。事后再找找解釋這些知識(shí)點(diǎn)的資料看看,自己解惑一下。
聊遠(yuǎn)了,下面說(shuō)下答案哈,如果用 Map 當(dāng)函數(shù)參數(shù),Map發(fā)生擴(kuò)容后,函數(shù)內(nèi)外的Map變量指向的底層內(nèi)存仍是一致的。這是為什么呢?答案我是在《Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn)》哈希表這一章找到的,有書(shū)的可以翻開(kāi) 75 頁(yè)看看。
如果沒(méi)有書(shū)的可以看文末的引用鏈接里貼的在線書(shū)籍地址。
關(guān)于 Map 的初始化是這么描述的
使用 make 創(chuàng)建哈希,Go 語(yǔ)言編譯器都會(huì)在類(lèi)型檢查期間將它們轉(zhuǎn)換成 runtime.makemap,使用字面量初始化哈希也只是語(yǔ)言提供的輔助工具,最后調(diào)用的都是 runtime.makemap:
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
......
return h
}
通過(guò)上面的解釋和代碼我們了解到 Map 這個(gè)數(shù)據(jù)類(lèi)型,在運(yùn)行時(shí)實(shí)際上是一個(gè) hmap類(lèi)型的指針,只不過(guò)在我們寫(xiě)代碼階段被隱藏起來(lái)了。
既然是一個(gè) Map 類(lèi)型的變量實(shí)際上是一個(gè)指針變量,這跟 Slice 就完全不同了,雖然指針作為函數(shù)參數(shù)時(shí)在 Go 里面也是按照值傳遞的,但是內(nèi)外兩個(gè)指針是指向的同一個(gè) hamp 結(jié)構(gòu)所在的內(nèi)存,hmap 結(jié)構(gòu)里有很多字段,回答這里的問(wèn)題,我們只需要知道 buckets 和 oldbuckets 這兩個(gè)指針類(lèi)型的字段就行了。
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
Go 的 Map中用于存儲(chǔ)鍵值對(duì)數(shù)據(jù)的結(jié)構(gòu)--桶(bmap),對(duì)于bmap 我們不再深挖下去。
buckets 是指向桶數(shù)組的。當(dāng)哈希表增長(zhǎng)到需要擴(kuò)容的時(shí)候,Go語(yǔ)言會(huì)將bucket數(shù)組的數(shù)量擴(kuò)充一倍,產(chǎn)生一個(gè)新的bucket數(shù)組,老數(shù)據(jù)存放在 oldbuckets 指向的桶中,并在被訪問(wèn)到時(shí)遷移到新桶中去。
這里雖然擴(kuò)容導(dǎo)致 Map 有了新 bucket 數(shù)組的地址,但是這個(gè)地址是存在 hmap 的字段 buckets 上的,變更字段的值并不會(huì)影響 hmap本身的內(nèi)存地址。
所以當(dāng) Map 由于函數(shù)內(nèi)的操作發(fā)生擴(kuò)容時(shí),不會(huì)像上面例子里的 Slice 指向不同底層數(shù)組的詭異現(xiàn)象。
不知道大家有沒(méi)有看明白我這里的分析,這篇文章其實(shí)是我自己對(duì)思考問(wèn)題的一個(gè)記錄,防止時(shí)間長(zhǎng)了以后忘掉。傳值、傳引用這些在不同的語(yǔ)言里不一樣,對(duì)于像我們掌握了至少三門(mén)編程語(yǔ)言的男人:)也就只能靠寫(xiě)寫(xiě)筆記防止混淆啦。
(我相信絕大多數(shù)人的職業(yè)生涯是不能靠一門(mén)編程語(yǔ)言吃遍天的)
還有一點(diǎn)我是覺(jué)得 Go 的 Slice 使用起來(lái)確實(shí)要耗費(fèi)的心智有點(diǎn)高,一不注意就容易踩坑,時(shí)間長(zhǎng)了,搞的大家用 Map 和 指針當(dāng)參數(shù)時(shí)也會(huì)先自我懷疑一下,希望這篇文章對(duì)解決掉你們的使用疑慮有一定幫助。
引用地址
Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn) --哈希表 https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap
本文標(biāo)題:Go函數(shù)的Map型參數(shù),會(huì)發(fā)生擴(kuò)容后指向不同底層內(nèi)存的事兒?jiǎn)幔?
本文地址:http://m.5511xx.com/article/cdcichd.html


咨詢
建站咨詢
