切片傳參的幻覺(jué) - 傳引用
golang中函數(shù)的參數(shù)為切片時(shí)是傳引用還是傳值?對(duì)于這個(gè)問(wèn)題,當(dāng)你百度一輪過(guò)后,你會(huì)發(fā)現(xiàn)很大一部分人認(rèn)為是傳引用,通常他們會(huì)貼出下面這段代碼進(jìn)行佐證:

上面代碼中,在main函數(shù)里邊初始化一個(gè)切片變量slice,接著調(diào)用changeSlice函數(shù),參數(shù)為切片變量slice。而函數(shù)changeSlice的主要處理邏輯是改變切片的第二個(gè)元素的值。下面我們看一下運(yùn)行打印的結(jié)果:
slice:[0123]slice:[011123]
從輸出結(jié)果我們看到,函數(shù)changeSlice內(nèi)對(duì)切片的修改,main函數(shù)中的切片變量slice也跟著修改了。咋一看,這不就是引用傳遞的表現(xiàn)嗎?
但事實(shí)上真的傳引用嗎?
理清三個(gè)重要概念
在探討函數(shù)切片參數(shù)到底是以哪種方式傳遞時(shí),我們先來(lái)理清下面三個(gè)重要的概念:
傳值(值傳遞)
穿指針
傳引用(引用傳遞)
傳值(值傳遞)
是指在調(diào)用函數(shù)時(shí)將實(shí)際參數(shù)拷貝一份傳遞到函數(shù)中,這樣在函數(shù)中對(duì)參數(shù)進(jìn)行修改不會(huì)影響到實(shí)際參數(shù)。這個(gè)簡(jiǎn)單不必贅述。
傳指針
形參是指向?qū)崊⒌刂返闹羔?,?dāng)對(duì)形參的指向進(jìn)行操作時(shí),就相當(dāng)于對(duì)實(shí)參本身進(jìn)行操作。聽(tīng)起來(lái)比較繞是吧,我們來(lái)看個(gè)例子就知道了:

上面代碼中定義了一個(gè)變量 a,并把地址保存在指針變量pa里面;接著打印pa的值和pa的地址,然后調(diào)用modify函數(shù),參數(shù)為指針變量pa;modify函數(shù)中首先打印形參p的值和p的地址,接著修改p的值為1。
我們打印輸出的結(jié)果:

從輸出結(jié)果中我們可以看到,這是一個(gè)指針的拷貝。指針pa 和 p 的值雖然相同,但是存放這兩個(gè)指針的內(nèi)存地址是不同的,因此這是兩個(gè)不同的指針。
注意:任何存放在內(nèi)存里的東西都有自己的地址,指針也不例外,它雖然指向別的數(shù)據(jù),但是也有存放該指針的內(nèi)存。
結(jié)合圖來(lái)看相信會(huì)更清晰一點(diǎn):

傳引用(引用傳遞)
是指在調(diào)用函數(shù)時(shí)將實(shí)際參數(shù)的地址傳遞到函數(shù)中,在函數(shù)中對(duì)參數(shù)所進(jìn)行的修改,將影響實(shí)際參數(shù)。
假設(shè)以上面demo為例子,如果在modify函數(shù)中打印指針變量p的地址也是0xc00000e028,那么我們就認(rèn)為是引用傳遞
但這里我們不能用go來(lái)舉例子,原因請(qǐng)接著往下看。
官方打假:Go函數(shù)傳參只有值傳遞
看完傳值、傳指針、傳引用的概念后,如果你堅(jiān)持認(rèn)為是傳引用,好,大叔要在這里直接把你擊垮。
根據(jù)Go官方文檔聲明:Go里面函數(shù)傳參只有值傳遞一種方式。也就是說(shuō),在Go中,函數(shù)的傳參只有傳值一種方式。官方文檔傳送門
如果你還不服氣,咱們直接看例子:

打印輸出:

如果函數(shù)切片參數(shù)傳的是引用,那么上面這個(gè)例子中,main函數(shù)中打印的切片slice的地址應(yīng)該和changSlice函數(shù)中打印切片的地址一樣的,但從輸出結(jié)果來(lái)看并不是這樣的。
因此在這里,我們可以非??隙ǖ卣f(shuō):有關(guān)Go函數(shù)中切片參數(shù)是傳引用的說(shuō)法是錯(cuò)誤的,另外有關(guān)有關(guān)引用傳遞是針對(duì)slice、map、channel三種數(shù)據(jù)類型的說(shuō)法也是錯(cuò)誤的
切片參數(shù)本質(zhì)還是傳值
從上面的分析來(lái)看,切片參數(shù)傳遞的方式并非是傳引用。反而極有可能是傳指針,而在傳指針那小節(jié)的分析中我們可以知道,傳指針其實(shí)就是指針的拷貝,形參和實(shí)參是兩個(gè)不同的指針,但是它們的值是一樣的。本質(zhì)上可以說(shuō)還是傳值。
那到底是不是這樣的呢?大叔疑問(wèn)句都出來(lái)了,說(shuō)明90%是有可能的,接下我們就驗(yàn)證一下那剩下的10%。
slice的結(jié)構(gòu)體
我們先來(lái)看一下切片的結(jié)構(gòu)體是長(zhǎng)啥樣的:

切片,顧名思義就是數(shù)組切下來(lái)的一部分,其結(jié)構(gòu)體包含了三部分,第一部分是指向底層數(shù)組的指針,其次是切片的大小len和切片的容量cap。(果然含有指針成員變量。)
上面的結(jié)構(gòu)體看著有點(diǎn)變扭,不夠直觀,我們?cè)靷€(gè)例子:一個(gè)數(shù)組 arr := [5]int{0,1,2,3,4},生成一個(gè)切片 slice := arr[1:4],最終得到的切片如下:

再看個(gè)例子:
func?main()?{????arr?:=?[5]int{0,1,2,3,4}????slice1?:=?arr[1:4]????slice2?:=?arr[2:5]????//?打印一????fmt.Printf("arr?%v,?slice1?%v,?slice2?%v???arr?addr:?%p,?slice1?addr:?%p,?slice2?addr:?%p\n",?arr,?slice1,?slice2,?&arr,?&slice1,?&slice2)????//?打印二????fmt.Printf("arr[2]?addr:?%p,?slice1[1]?addr:?%p,?slice2[0]?addr:?%p\n",?&arr[2],?&slice1[1],?&slice2[0])????arr[2]?=2222//?打印三????fmt.Printf("arr:?%v,?slice1:?%v,?slice2:?%v\n",?arr,?slice1,?slice2)????slice1[1]?=1111//?打印四????fmt.Printf("arr:?%v,?slice1:?%v,?slice2:?%v\n",?arr,?slice1,?slice2)}
上面代碼中我們創(chuàng)建一個(gè)數(shù)組,并生成兩個(gè)切片。打印它們的值和對(duì)應(yīng)的地址。另外,修改數(shù)組或者切片的某個(gè)單元的值,觀察數(shù)組和切片中單元的值的變化:
arr[01234],slice1[123],slice2[234]arraddr:0xc000014090,slice1addr:0xc00000c080,slice2addr:0xc00000c0a0arr[2]addr:0xc0000140a0,slice1[1]addr:0xc0000140a0,slice2[0]addr:0xc0000140a0arr:[01222234],slice1:[122223],slice2:[222234]arr:[01111134],slice1:[111113],slice2:[111134]
從打印一結(jié)果可以看出:創(chuàng)建的兩個(gè)切片,它們各自擁有不同的地址
從打印二結(jié)果可以看出:切片元素slice1[1]、slice2[0] 與數(shù)組元素arr[2]有著同樣的地址,說(shuō)明這些切片共享著數(shù)組arr中的數(shù)據(jù)
打印三和打印四可以看出:修改數(shù)組和切片共同部分的數(shù)據(jù),對(duì)兩者都有直接影響,再次印證第二點(diǎn)的結(jié)論。
從上面的分析中我們可以知道,兩個(gè)不同的切片之所以能相互影響,主要因素是切片內(nèi)部的指針指向同一個(gè)數(shù)據(jù)源,且兩個(gè)切片的指針指向的數(shù)據(jù)源中有交集。
再回到切片作為函數(shù)參數(shù)的問(wèn)題上,因?yàn)?b>Go里面函數(shù)傳參只有值傳遞一種方式,所以當(dāng)切片作為參數(shù)時(shí),其實(shí)也是切片的拷貝,但是在拷貝的切片中,其包含的指針成員變量的值是一樣的,也就是說(shuō)它們指向的數(shù)據(jù)源是一樣,因此在調(diào)用函數(shù)內(nèi)修改形參能影響實(shí)參。
Go函數(shù)傳值總結(jié)
通常,我們把在傳值拷貝過(guò)程中,修改形參能直接修改實(shí)參的數(shù)據(jù)類型稱為引用類型。
于是我們又可以這樣總結(jié):
Go語(yǔ)言中所有的傳參都是值傳遞(傳值),都是一個(gè)副本,一個(gè)拷貝。因?yàn)榭截惖膬?nèi)容有時(shí)候是非引用類型(int、string、struct等這些),這樣就在函數(shù)中就無(wú)法修改原內(nèi)容數(shù)據(jù);有的是引用類型(指針、map、slice、chan等這些),這樣就可以修改原內(nèi)容數(shù)據(jù)。
這里要注意的是:引用類型和傳引用是兩個(gè)概念。
切片參數(shù),修改形參一定影響實(shí)參嗎
是的,看到這里,我們應(yīng)該問(wèn)自己一個(gè)問(wèn)題:函數(shù)的切片參數(shù),在函數(shù)內(nèi)修改形參一定會(huì)影響外部的實(shí)參嗎?(疑問(wèn)句一出必然有鬼?。?/p>
我們來(lái)看個(gè)例子:

上面代碼中,先是創(chuàng)建好一個(gè)長(zhǎng)度為2,容量為3的空切片,然后給切片賦兩個(gè)值,接著調(diào)用函數(shù)changeSlice,參數(shù)為賦完值的切片。函數(shù)changeSlice里面先是往切片中追加兩個(gè)元素,然后再改變切片的第二個(gè)元素的值。
我們來(lái)看看打印結(jié)果:
slice:[01],addr:0xc00001a100funcs:[011134],addr:0xc000014090slice:[01],addr:0xc00001a100
從輸出結(jié)果我們可以看到,函數(shù)內(nèi)修改形參s后,外部的實(shí)參slice并沒(méi)有跟著改變,而且注意到一點(diǎn)是,形參s和實(shí)參slice的指針成員變量的值是不一樣的,也就是說(shuō)它們指向的數(shù)據(jù)源不是同一個(gè)了。
這又是為什么呢?
切片擴(kuò)容
首先我們來(lái)了解一個(gè)知識(shí)點(diǎn):
無(wú)論數(shù)組還是切片,都有長(zhǎng)度限制。也就是追加切片的時(shí)候,如果元素正好在切片的容量范圍內(nèi),直接在尾部追加一個(gè)元素即可。如果超出了最大容量,再追加元素就需要針對(duì)底層的數(shù)組進(jìn)行復(fù)制和擴(kuò)容操作了。
也就是說(shuō):
使用append方法給slice追加元素的時(shí)候,由于slice的容量還未滿,因此等同于擴(kuò)展了slice指向數(shù)組的內(nèi)容,可以理解為重新切了一個(gè)數(shù)組內(nèi)容附給slice,同時(shí)修改了數(shù)組的內(nèi)容。
使用append方法給slice追加元素的時(shí)候,如果此時(shí)slice的容量已滿,再進(jìn)行追加時(shí),超出了切片的容量,數(shù)組就會(huì)越界了,于是就會(huì)出現(xiàn)擴(kuò)容操作
當(dāng)需要擴(kuò)容時(shí),append會(huì)做哪些操作呢?
創(chuàng)建一個(gè)新的臨時(shí)切片t,t的長(zhǎng)度和slice切片的長(zhǎng)度一樣,但是t的容量是slice切片的2倍,新建切片的時(shí)候,底層也創(chuàng)建了一個(gè)匿名的數(shù)組,數(shù)組的長(zhǎng)度和切片容量一樣。
復(fù)制slice里面的元素到t里,即填入匿名數(shù)組中。然后把t賦值給slice,現(xiàn)在slice的指向了底層的匿名數(shù)組。
轉(zhuǎn)變成小于容量的append方法。
舉個(gè)例子,數(shù)組arr = [3]int{0, 11, 22},生成一個(gè)切片slice := arr[1:3],使用append方法往切片slice中追加元素33,將發(fā)生以下操作:

再回到剛剛舉的例子,之所外部的實(shí)參切片變量slice不受形參切片變量s修改的影響,因?yàn)樵趫?zhí)行完?s = append(s, 4)?這段代碼后,切片s的指針指向的數(shù)組發(fā)生了擴(kuò)容,其指針指向了新的數(shù)組,因此當(dāng)再次修改其第二個(gè)元素的值時(shí),是不會(huì)影響外部切片變量slice的。
切片容量計(jì)算
具體的切片容量計(jì)算可參考下面的例子:
數(shù)組 [0, 1, 2, 3, 4] 中,數(shù)組有5個(gè)元素。如果切片 s = [1, 2, 3],那么3在數(shù)組的索引為3,也就是數(shù)組還剩最后一個(gè)元素的大小,加上s已經(jīng)有3個(gè)元素,因此最后s的容量為 1 + 3 = 4。如果切片是 s1 = [4],4的索引在數(shù)組中是最大的了,數(shù)組空余的元素為0,那么s1的容量為 0 + 1 = 1。具體如下表:

作業(yè)
好了,寫(xiě)到這里本次的分享就要接近尾聲了,最后想想還是有必要留個(gè)作業(yè)給大家,檢驗(yàn)一下大家對(duì)以上知識(shí)點(diǎn)的接受程度:

大家在評(píng)論中寫(xiě)下你的答案吧~
轉(zhuǎn)載:
https://juejin.im/post/6888117219213967368