切片 slice 原理分析

image

前言

作為一個(gè) Go 語言新手,看到一切”詭異“的代碼都會(huì)感到好奇量蕊;比如我最近看到的幾個(gè)方法铺罢;偽代碼如下:

func FindA() ([]*T,error) {
}

func FindB() ([]T,error) {
}

func SaveA(data *[]T) error {
}

func SaveB(data *[]*T) error {
}

相信大部分剛?cè)腴T Go 的新手看到這樣的代碼也是一臉懵逼,其中最讓人疑惑的就是:

[]*T
*[]T
*[]*T

這樣對(duì)切片的聲明残炮,先不看后面兩種寫法韭赘;單獨(dú)看 []*T 還是很好理解的:
該切片中存放的是所有 T 的內(nèi)存地址,會(huì)比存放 T 本身來說要更省空間势就,同時(shí) []*T 在方法內(nèi)部是可以修改 T 的值泉瞻,而[]T 是修改不了。

func TestSaveSlice(t *testing.T) {
    a := []T{{Name: "1"}, {Name: "2"}}
    for _, t2 := range a {
        fmt.Println(t2)
    }
    _ = SaveB(a)
    for _, t2 := range a {
        fmt.Println(t2)
    }

}
func SaveB(data []T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}

type T struct {
    Name string
}

比如以上例子打印的是

{1}
{2}
{1}
{2}

只有將方法修改為

func SaveB(data []*T) error {
    t := data[0]
    t.Name = "1233"
    return nil
}

才能修改 T 的值:

&{1}
&{2}
&{1233}
&{2}

示例

下面重點(diǎn)來看看 []*T*[]T 的區(qū)別苞冯,這里寫了兩個(gè) append 函數(shù):

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}

先看第一種袖牙,輸出是結(jié)果是:

appendA [1000 2 3]
main [1000 2 3]

說明在函數(shù)傳遞過程中,函數(shù)內(nèi)部的修改能夠影響到外部舅锄。


下面我們?cè)倏匆粋€(gè)例子:

func appendB(x []int) {
    x = append(x, 4)
    fmt.Printf("appendA %v\n", x)
}

最終結(jié)果卻是:

appendA [1 2 3 4]
main [1 2 3]

沒有影響到外部鞭达。

而當(dāng)我們?cè)僬{(diào)整一下會(huì)發(fā)現(xiàn)又有所不同:

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v\n", x)
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendA %v\n", x)
}

最終的結(jié)果:

appendA &[1 2 3 4]
main [1 2 3 4]

可以發(fā)現(xiàn)如果傳遞切片的指針時(shí),使用 append 函數(shù)追加數(shù)據(jù)時(shí)會(huì)影響到外部皇忿。

slice 原理

在分析上面三種情況之前畴蹭,我們先來了解下 slice 的數(shù)據(jù)結(jié)構(gòu)。

直接查看源碼會(huì)發(fā)現(xiàn) slice 其實(shí)就是一個(gè)結(jié)構(gòu)體鳍烁,只是不能直接對(duì)外訪問叨襟。

image

源碼地址 runtime/slice.go

其中有三個(gè)重要的屬性:

屬性 含義
array 底層存放數(shù)據(jù)的數(shù)組,是一個(gè)指針老翘。
len 切片長(zhǎng)度
cap 切片容量 cap>=len

提到切片就不得不想到數(shù)組芹啥,可以這么理解:

切片是對(duì)數(shù)組的抽象,而數(shù)組則是切片的底層實(shí)現(xiàn)铺峭。

其實(shí)通過切片這個(gè)名字也不難看出墓怀,它就是從數(shù)組中切了一部分;相對(duì)于數(shù)組的固定大小卫键,切片可以根據(jù)實(shí)際使用情況進(jìn)行擴(kuò)容傀履。

所以切片也可以通過對(duì)數(shù)組"切一刀"獲得:

x1:=[6]int{0,1,2,3,4,5}
x2 := x[1:4]
fmt.Println(len(x2), cap(x2))
image

其中 x1 的長(zhǎng)度與容量都是6。

x2 的長(zhǎng)度與容量則為3和5。

  • x2 的長(zhǎng)度很容易理解钓账。
  • 容量等于5可以理解為碴犬,當(dāng)前這個(gè)切片最多可以使用的長(zhǎng)度。

因?yàn)榍衅?x2 是對(duì)數(shù)組 x1 的引用梆暮,所以底層數(shù)組排除掉左邊一個(gè)沒有被引用的位置則是該切片最大的容量服协,也就是5。

同一個(gè)底層數(shù)組

以剛才的代碼為例:

func TestAppendA(t *testing.T) {
    x:=[]int{1,2,3}
    appendA(x)
    fmt.Printf("main %v\n", x)
}
func appendA(x []int) {
    x[0]= 100
    fmt.Printf("appendA %v\n", x)
}
image

在函數(shù)傳遞過程中啦粹,main 中的 x 與 appendA 函數(shù)中的 x 切片所引用的是同個(gè)數(shù)組偿荷。

所以在函數(shù)中對(duì) x[0]=100main函數(shù)中也能獲取到唠椭。

image

本質(zhì)上修改的就是同一塊內(nèi)存數(shù)據(jù)跳纳。

值傳遞帶來的誤會(huì)

在上述例子中,在 appendB 中調(diào)用 append 函數(shù)追加數(shù)據(jù)后會(huì)發(fā)現(xiàn) main 函數(shù)中并沒有受到影響贪嫂,這里我稍微調(diào)整了一下示例代碼:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}

主要是修改了切片初始化方式寺庄,使得容量大于了長(zhǎng)度,具體原因后續(xù)會(huì)說明力崇。

輸出結(jié)果如下:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5

main 函數(shù)中的數(shù)據(jù)看樣子確實(shí)沒有受到影響斗塘;但細(xì)心的朋友應(yīng)該會(huì)注意到 appendB 函數(shù)中的 x 在 append() 之后長(zhǎng)度 +1 變?yōu)榱?。

而在 main 函數(shù)中長(zhǎng)度又變回了3.

這個(gè)細(xì)節(jié)區(qū)別就是為什么 append() "看似" 沒有生效的原因餐曹;至于為什么要說“看似”逛拱,再次調(diào)整了代碼:

func TestAppendB(t *testing.T) {
    //x:=[]int{1,2,3}
    x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}

在剛才的基礎(chǔ)之上,以 append 之后的 x 為基礎(chǔ)再做了一個(gè)切片台猴;該切片的范圍為 x 所引用數(shù)組的全部數(shù)據(jù)朽合。

再來看看執(zhí)行結(jié)果如何:

appendB [1 2 3 444] len=4,cap=5
main [1 2 3] len=3,cap=5
y [1 2 3 444 0] len=5,cap=5

會(huì)神奇的發(fā)現(xiàn) y 將所有數(shù)據(jù)都打印出來,在 appendB 函數(shù)中追加的數(shù)據(jù)其實(shí)已經(jīng)寫入了數(shù)組中饱狂,但為什么 x 本身沒有獲取到呢曹步?

image

看圖就很容易理解了:

  • appendB中確實(shí)是對(duì)原始數(shù)組追加了數(shù)據(jù),同時(shí)長(zhǎng)度也增加了休讳。
  • 但由于是值傳遞讲婚,所以 slice 這個(gè)結(jié)構(gòu)體即便是修改了長(zhǎng)度為4,也只是對(duì)復(fù)制的那個(gè)對(duì)象修改了長(zhǎng)度俊柔,main 中的長(zhǎng)度依然為3.
  • 由于底層數(shù)組是同一個(gè)筹麸,所以基于這個(gè)底層數(shù)組重新生成了一個(gè)完整長(zhǎng)度的切片便能看到追加的數(shù)據(jù)了。

所以這里本質(zhì)的原因是因?yàn)?slice 是一個(gè)結(jié)構(gòu)體雏婶,傳遞的是值物赶,不管方法里如何修改長(zhǎng)度也不會(huì)影響到原有的數(shù)據(jù)(這里指的是長(zhǎng)度和容量這兩個(gè)屬性)。

切片擴(kuò)容

還有一個(gè)需要注意:

剛才特意提到這里的例子稍有改變留晚,主要是將切片的容量設(shè)置超過了數(shù)組的長(zhǎng)度酵紫;

如果不做這個(gè)特殊設(shè)置會(huì)怎么樣呢?

func TestAppendB(t *testing.T) {
    x:=[]int{1,2,3}
    //x := make([]int, 3,5)
    x[0] = 1
    x[1] = 2
    x[2] = 3
    appendB(x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))

    y:=x[0:cap(x)]
    fmt.Printf("y %v len=%v,cap=%v\n", y,len(y),cap(y))
}
func appendB(x []int) {
    x = append(x, 444)
    fmt.Printf("appendB %v len=%v,cap=%v\n", x,len(x),cap(x))
}

輸出結(jié)果:

appendB [1 2 3 444] len=4,cap=6
main [1 2 3] len=3,cap=3
y [1 2 3] len=3,cap=3

這時(shí)會(huì)發(fā)現(xiàn) main 函數(shù)中的 y 切片數(shù)據(jù)也沒有發(fā)生變化,這是為什么呢奖地?

image

這是因?yàn)槌跏蓟?x 切片時(shí)長(zhǎng)度和容量都為3橄唬,當(dāng)在 appendB 函數(shù)中追加數(shù)據(jù)時(shí),會(huì)發(fā)現(xiàn)沒有位置了参歹。

這時(shí)便會(huì)進(jìn)行擴(kuò)容:

  • 將老數(shù)據(jù)復(fù)制一份到新的數(shù)組中仰楚。
  • 追加數(shù)據(jù)。
  • 將新的數(shù)據(jù)內(nèi)存地址返回給 appendB 中的 x .

同樣的由于是值傳遞泽示,所以 appendB 中的切片換了底層數(shù)組對(duì) main 函數(shù)中的切片沒有任何影響缸血,也就導(dǎo)致最終 main 函數(shù)的數(shù)據(jù)沒有任何變化了蜜氨。

傳遞切片指針

有沒有什么辦法即便是在擴(kuò)容時(shí)也能對(duì)外部產(chǎn)生影響呢械筛?

func TestAppendC(t *testing.T) {
    x:=[]int{1,2,3}
    appendC(&x)
    fmt.Printf("main %v len=%v,cap=%v\n", x,len(x),cap(x))
}
func appendC(x *[]int) {
    *x = append(*x, 4)
    fmt.Printf("appendC %v\n", x)
}

輸出結(jié)果為:

appendC &[1 2 3 4]
main [1 2 3 4] len=4,cap=6

這時(shí)外部的切片就能受到影響了,其實(shí)原因也很簡(jiǎn)單飒炎;

剛才也說了埋哟,因?yàn)?slice 本身是一個(gè)結(jié)構(gòu)體,所以當(dāng)我們傳遞指針時(shí)郎汪,就和平時(shí)自定義的 struct 在函數(shù)內(nèi)部通過指針修改數(shù)據(jù)原理相同赤赊。

最終在 appendC 中的 x 的指針指向了擴(kuò)容后的結(jié)構(gòu)體,因?yàn)閭鬟f的是 main 函數(shù)中 x 的指針煞赢,所以同樣的 main 函數(shù)中的 x 也指向了該結(jié)構(gòu)體抛计。

總結(jié)

所以總結(jié)一下:

  • 切片是對(duì)數(shù)組的抽象,同時(shí)切片本身也是一個(gè)結(jié)構(gòu)體照筑。
  • 參數(shù)傳遞時(shí)函數(shù)內(nèi)部與外部引用的是同一個(gè)數(shù)組吹截,所以對(duì)切片的修改會(huì)影響到函數(shù)外部。
  • 如果發(fā)生擴(kuò)容凝危,情況會(huì)發(fā)生變化波俄,同時(shí)擴(kuò)容會(huì)導(dǎo)致數(shù)據(jù)拷貝;所以要盡量預(yù)估切片大小蛾默,避免數(shù)據(jù)拷貝懦铺。
  • 對(duì)切片或數(shù)組重新生成切片時(shí),由于共享的是同一個(gè)底層數(shù)組支鸡,所以數(shù)據(jù)會(huì)互相影響冬念,這點(diǎn)需要注意。
  • 切片也可以傳遞指針牧挣,但場(chǎng)景很少急前,還會(huì)帶來不必要的誤解;建議值傳值就好浸踩,長(zhǎng)度和容量占用不了多少內(nèi)存叔汁。

相信使用過切片會(huì)發(fā)現(xiàn)非常類似于 Java 中的 ArrayList,同樣是基于數(shù)組實(shí)現(xiàn),也會(huì)擴(kuò)容發(fā)生數(shù)據(jù)拷貝据块;這樣看來語言只是上層使用的選擇码邻,一些通用的底層實(shí)現(xiàn)大家都差不多。

這時(shí)我們?cè)倏礃?biāo)題中的 []*T *[]T *[]*T 就會(huì)發(fā)現(xiàn)這幾個(gè)并沒有什么聯(lián)系另假,只是看起來很像容易唬人像屋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市边篮,隨后出現(xiàn)的幾起案子己莺,更是在濱河造成了極大的恐慌,老刑警劉巖戈轿,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凌受,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡思杯,警方通過查閱死者的電腦和手機(jī)胜蛉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來色乾,“玉大人誊册,你說我怎么就攤上這事∨担” “怎么了案怯?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)澎办。 經(jīng)常有香客問我嘲碱,道長(zhǎng),這世上最難降的妖魔是什么浮驳? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任悍汛,我火速辦了婚禮,結(jié)果婚禮上至会,老公的妹妹穿的比我還像新娘离咐。我一直安慰自己,他們只是感情好奉件,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布宵蛀。 她就那樣靜靜地躺著,像睡著了一般县貌。 火紅的嫁衣襯著肌膚如雪术陶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天煤痕,我揣著相機(jī)與錄音梧宫,去河邊找鬼接谨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛塘匣,可吹牛的內(nèi)容都是我干的脓豪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼忌卤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼扫夜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起驰徊,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤笤闯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后棍厂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體颗味,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年勋桶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了脱衙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡例驹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出退唠,到底是詐尸還是另有隱情鹃锈,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布瞧预,位于F島的核電站屎债,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏垢油。R本人自食惡果不足惜盆驹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望滩愁。 院中可真熱鬧躯喇,春花似錦、人聲如沸硝枉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妻味。三九已至正压,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間责球,已是汗流浹背焦履。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工拓劝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嘉裤。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓凿将,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親价脾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子牧抵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容