Go中slice做爲參數傳遞的一些「坑」

看到這個題目,可能你們都以爲是一個老生常談的月經topic了。一直以來其實把握一個「值傳遞」基本上就能理解各類狀況了,不過最近遇到了更深一點的「小坑」,與你們分享一下。golang

首先仍是從最簡單的提及,看下面代碼:數組

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)
}
複製代碼

能夠點擊這裏運行代碼,以上代碼的輸出是什麼呢?數據結構

我這裏不賣關子了直接說,再調用ap函數進行append操做後,a依然是[]int{7,8,9}。緣由很簡單,Go中沒有引用傳遞全是值傳遞,值傳遞意味着傳遞的是數據的拷貝。這句話新手可能稍微有點雲裏霧裏,而實際狀況又比較詭異,好比說下面代碼:app

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)
}
複製代碼

點擊這裏運行代碼,這時ap後再輸出a,會看到a[0]變成了1,但a的cap依然是3,看起來10並無被append進去?函數

這看起來就比較匪夷所思了,不是說值傳遞嗎,爲何仍是影響外部變量的值了呢?按理說要麼都變要麼都不變才說得過去啊。ui

這實際上並非匪夷所思,由於Go和C不同,slice看起來像數組,其實是一個結構體,在源碼中的數據結構是:spa

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
複製代碼

這個結構體其實也很好理解,array是一個真正的數組指針,指向一段連續內存空間的頭部,len和cap表明長度和容量。指針

換句話說,你看起來在代碼裏傳參時寫的是ap(a []int),實際上在代碼編譯期,這段代碼變成了ap(a runtime.slice)code

你能夠嘗試這麼理解,把ap(a)替換成ap(array: 0x123, len: 3, cap: 3)。能夠很明顯的看到,傳遞到ap函數的三個參數,僅僅是3個數值,並無和外部變量a創建任何引用關係。這即是值傳遞。server

可是,你可能會疑惑,爲何我改了a[0]的值,也會在外面體現呢?其實看到這裏你應該已經能夠本身想明白了,由於array是一個地址值(好比0x123),這個地址傳入了ap函數,可是它表明的地址0x123和外部a的0x123是一個內存地址,這時候你修改a[0],其實是修改0x123地址中存放的值,因此外部固然會受影響了。

舉個形象點的例子,假設你是火車站貨物管理員,你管理的是第1到第3節車箱(車箱是互通的)的裝卸貨貨。有一天你生病了,找我的(叫A)臨時來接手一下。可是火車的貨不是誰想碰就碰的,你得有證實才行。因而你把你手上的證實原件複印了一份給A,同時把第一節車箱的鑰匙給A。因爲恰好那幾天比較忙,站長又讓A也負責第四節車箱,因而A也獲得了車箱4的證實原件。一段時間後,你生病回來,你依然只有1到3節車箱的證件,你能夠看到最近A在1到3車箱搞的事情,可是你沒有資格去4車箱。

以上例子應該能夠很好的說明slice傳參的場景,記住,Go中只有值傳遞。

是否是就完事兒了呢?然而事情並無這麼簡單。最近我工做時就遇到這個問題了。按照上面的舉例,雖然你沒有資格去查看4車箱,可是若是你好奇,你能夠偷看啊,由於它們是連續的互通的,正如數組也是一段連續的內存,因而就有這樣的代碼:

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並無改變,可是ap函數往同一段內存地址append了一個10,那我是否是能夠用比較trick的方法去偷看呢?好比找到a[2]的地址,日後挪一個int的長度,就應該是ap函數新增的10了吧?這裏須要注意,Go官網的server是32位的,因此在go playground執行這段代碼時,int是4字節。

執行結果和我預想的同樣!

可是問題接踵而至

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)
}
複製代碼

這段代碼你再試試

這和上面一個例子惟一的區別就是slice一開始是用[]int{7,8,9}這種方式初始化。執行結果*t是3而不是10,這就比較困惑了。爲啥?不是一段連續的內存空間嗎?

這裏其實涉及到的問題是slice的growth問題,當append時發現cap不夠了,會從新分配空間,具體源碼參見 runtime/slice.go中的growslice函數。我這裏就不講太多細節,只講結果。當發生growslice時,會給slice從新分配一段更大的內存,而後把原來的數據copy過去,把slice的array指針指向新內存。也就是說,假如以前的數據是存放到內存地址 0x0 0x8 0x10,當不發生growslice,新append的數值會存到0x18,然而當發生growslice,之前的全部數據被copy到新的地址0x1000 0x1008 0x1010,新append的值放到0x1018了。

這時候你就能夠理解爲何有時候用unsafe能拿到數據,有時候拿不到了。或許你能夠理解爲何這個包叫作unsafe了。不過unsafe不是真的unsafe,是說若是你使用的姿式不對就很是容易unsafe。可是若是姿式優雅,其實很safe。對於slice操做,若是要使用unsafe,千萬記得關注cap是否發送變化,它意味着內存的遷移

相關文章
相關標籤/搜索