一文詳解Go語言之Slice
一、什么是slice
slice翻譯成中文是切片的意思,而在go編程中slice是一個(gè)數(shù)據(jù)類型,其代表一個(gè)列表,類似于java中的List。我們可以為每一種go中的基礎(chǔ)類型或自定義類型創(chuàng)建對應(yīng)的切片。在這里我們可以將slice理解成一個(gè)列表,而在日常開發(fā)中不管是使用什么語言,都需要經(jīng)常用到列表這種數(shù)據(jù)結(jié)構(gòu),比如java中的List,我們在日常使用Java的開發(fā)中十分常見。而與java不同的是go將列表也就是slice作為一種基本類型,而不是List這樣的封裝類。
二、slice的結(jié)構(gòu)
slice結(jié)構(gòu)如下,其內(nèi)部存放了指向底層數(shù)組的指針、len(長度)、cap(容量)

三、slice 實(shí)現(xiàn)原理
Slice又稱動(dòng)態(tài)數(shù)組,依托數(shù)組實(shí)現(xiàn),可以方便的進(jìn)行擴(kuò)容、傳遞等,實(shí)際使用中比數(shù)組更靈活。Slice 依托數(shù)組實(shí)現(xiàn),底層數(shù)組對用戶屏蔽,在底層數(shù)組容量不足時(shí)可以實(shí)現(xiàn)自動(dòng)重分配并生成新的 Slice 。?
源碼包中 src/runtime/slice.go:slice 定義了 Slice 的數(shù)據(jù)結(jié)構(gòu):?
type?slice?struct?{?
?????array?unsafe.Pointer?
?????len???int?
?????cap???int
}?從數(shù)據(jù)結(jié)構(gòu)看Slice很清晰, array指針指向底層數(shù)組,len表示切片長度,cap表示底層數(shù)組容量。
3.1 使用make創(chuàng)建Slice
使用 make 來創(chuàng)建 Slice 時(shí),可以同時(shí)指定長度和容量,創(chuàng)建時(shí)底層會(huì)分配一個(gè)數(shù)組,數(shù)組的長度即容量。例如,語句 slice := make([]int, 5, 10) 所創(chuàng)建的 Slice ,結(jié)構(gòu)如下圖所示:

該 Slice 長度為 5 ,即可以使用下標(biāo) slice[0] ~ slice[4] 來操作里面的元素, capacity 為 10 ,表示后續(xù)向 slice 添加新的元素時(shí)可以不必重新分配內(nèi)存,直接使用預(yù)留內(nèi)存即可。
3.2 使用數(shù)組創(chuàng)建Slice
使用數(shù)組來創(chuàng)建 Slice 時(shí), Slice 將與原數(shù)組共用一部分內(nèi)存。例如,語句 slice := array[5:7] 所創(chuàng)建的 Slice ,結(jié)構(gòu)如下圖所示:

切片從數(shù)組 array[5] 開始,到數(shù)組 array[7] 結(jié)束(不含 array[7] ),即切片長度為 2 ,數(shù)組后面的內(nèi)容都作為切片的預(yù)留內(nèi)存, 即capacity 為 5 。
數(shù)組和切片操作可能作用于同一塊內(nèi)存,這也是使用過程中需要注意的地方。
四、Slice 擴(kuò)容
4.1、slice 擴(kuò)容影響原數(shù)組
append 函數(shù)會(huì)改變 slice 所引用的數(shù)組的內(nèi)容,從而影響到引用同一數(shù)組的其它 slice。但當(dāng) slice 中沒有剩余空間(即(cap-len) == 0)時(shí),此時(shí)將動(dòng)態(tài)分配新的數(shù)組空間。返回的slice 數(shù)組指針將指向這個(gè)空間,而原數(shù)組的內(nèi)容將保持不變;其它引用此數(shù)組的 slice 則不受影響。
func?testSlice()?{
?var?ar?=?[...]byte{'a',?'b',?'c',?'d',?'e',?'f',?'g',?'h'}
?//?打印結(jié)果:abcdefgh
?fmt.Printf("%s?\n",?ar)
?slice1?:=?ar[2:5]
?//?注:append 函數(shù)會(huì)改變 slice 所引用的數(shù)組的內(nèi)容,從而影響到引用同一數(shù)組的其它 slice。
?//?但當(dāng)?slice?中沒有剩余空間(即(cap-len)?==?0)時(shí),此時(shí)將動(dòng)態(tài)分配新的數(shù)組空間。返回的
?// slice 數(shù)組指針將指向這個(gè)空間,而原數(shù)組的內(nèi)容將保持不變;其它引用此數(shù)組的 slice 則不受影響。
?slice1?=?append(slice1,?'A')
?//?打印結(jié)果:cdeA
?fmt.Printf("%s?\n",?slice1)
?//?打印結(jié)果:abcdeAgh
?fmt.Printf("%s?\n",?ar)
}
4.2、slice 擴(kuò)容注意事項(xiàng)
使用 append 向 Slice 追加元素時(shí),如果 Slice 空間不足,將會(huì)觸發(fā) Slice 擴(kuò)容,擴(kuò)容實(shí)際上是重新分配一塊更大的內(nèi)存,將原Slice數(shù)據(jù)拷貝進(jìn)新 Slice ,然后返回新 Slice ,擴(kuò)容后再將數(shù)據(jù)追加進(jìn)去。例如,當(dāng)向一個(gè) capacity 為 5 ,且 length 也為 5 的 Slice 再次追加 1 個(gè)元素時(shí),就會(huì)發(fā)生擴(kuò)容,如下圖所示:

擴(kuò)容操作只關(guān)心容量,會(huì)把原 Slice 數(shù)據(jù)拷貝到新 Slice ,追加數(shù)據(jù)由 append 在擴(kuò)容結(jié)束后完成。上圖可見,擴(kuò)容后新的 Slice 長度仍然是 5 ,但容量由 5 提升到了 10 ,原 Slice 的數(shù)據(jù)也都拷貝到了新 Slice 指向的數(shù)組中。
4.3、擴(kuò)容容量的選擇遵循以下規(guī)則:
如果原Slice 容量小于 1024 ,則新 Slice 容量將擴(kuò)大為原來的 2 倍;
如果原Slice 容量大于等于 1024 ,則新 Slice 容量將擴(kuò)大為原來的 1.25 倍;
4.4、使用append()向Slice添加一個(gè)元素的實(shí)現(xiàn)步驟如下:
1、假如Slice 容量夠用,則將新元素追加進(jìn)去, Slice.len++ ,返回原 Slice?
2、原Slice 容量不夠,則將 Slice 先擴(kuò)容,擴(kuò)容后得到新 Slice?
3、將新元素追加進(jìn)新Slice , Slice.len++ ,返回新的 Slice 。
五、常見的slice的坑
slice到底是值傳遞還是引用傳遞?
對于這個(gè)問題我相信很多人都對此有爭議,我們先來看一段代碼:

在上面這段代碼中,我們定義了一個(gè)長度len為5的切片s,并將s賦值給了t,之后我們將t[0]的值修改成了99,最終我們發(fā)現(xiàn),s[0]的值也發(fā)生了改變,以當(dāng)前的現(xiàn)象來看slice是引用傳遞,我們先不急,再來看一段代碼。

在這段代碼中我們依然定義了一個(gè)長度為5,容量為10的切片s,并將s賦值給了t,然后向t中添加了一個(gè)元素6。分別打印s跟t,我們發(fā)現(xiàn)這次打印出來的內(nèi)容并不一致,從這個(gè)現(xiàn)象看來又好像是值傳遞。這時(shí)候可能有些人會(huì)有些疑惑,為什么同時(shí)表現(xiàn)出了值傳遞與引用傳遞的現(xiàn)象。我們再來看一段代碼。

這次我們在代碼2的基礎(chǔ)上分別打印了 s 與 t 的len、cap與底層數(shù)組的地址,我們發(fā)現(xiàn) s 的len為5,t 的len為6,除此之外cap與底層數(shù)組的地址都是一致的。因此我們可以得出結(jié)論,slice在go中的應(yīng)該是值傳遞,只不過當(dāng)將 s 賦值給 t 時(shí)底層數(shù)組指針指向的是同一個(gè)底層數(shù)組,而len與cap都是拷貝的副本,所以 t 在append之后len發(fā)生變化而 s 中的len并不會(huì)因此發(fā)生改變,其傳遞過程如下:

文章參考鏈接:
https://blog.csdn.net/xiaohei_buhei/article/details/122681538
https://blog.csdn.net/qq_40880022/article/details/123997549
