作者:李驍
12.1 切片(slice)
切片(slice) 是對(duì)底層數(shù)組一個(gè)連續(xù)片段的引用(該數(shù)組我們稱之為相關(guān)數(shù)組,通常是匿名的),所以切片是一個(gè)引用類型(和數(shù)組不一樣)。切片提供對(duì)該數(shù)組中編號(hào)的元素序列的訪問。 切片類型表示其元素類型的所有數(shù)組切片的集合。未初始化切片的值為nil。
與數(shù)組一樣,切片是可索引的并且具有長度。切片s的長度可以通過內(nèi)置函數(shù)len() 獲取;與數(shù)組不同,切片的長度可能在執(zhí)行期間發(fā)生變化。元素可以通過整數(shù)索引0到len(s)-1來尋址。我們可以把切片看成是一個(gè)長度可變的數(shù)組。
切片提供了計(jì)算容量的函數(shù) cap() ,可以測量切片最大長度。切片的長度永遠(yuǎn)不會(huì)超過它的容量,所以對(duì)于切片 s 來說,這個(gè)不等式永遠(yuǎn)成立:0 <= len(s) <= cap(s)。
一旦初始化,切片始終與保存其元素的基礎(chǔ)數(shù)組相關(guān)聯(lián)。因此,切片會(huì)和與其擁有同一基礎(chǔ)數(shù)組的其他切片共享存儲(chǔ);相比之下,不同的數(shù)組總是代表不同的存儲(chǔ)。
切片下面的數(shù)組可以延伸超過切片的末端。容量是切片長度與切片之外的數(shù)組長度的總和。
使用內(nèi)置函數(shù)make()可以給切片初始化,該函數(shù)指定切片類型和指定長度和可選容量的參數(shù)。
切片與數(shù)組相比較:
優(yōu)點(diǎn)
因?yàn)榍衅且?,所以它們不需要使用額外的內(nèi)存并且比使用數(shù)組更有效率,所以在 Go 代碼中切片比數(shù)組更常用。
聲明切片的格式是: var identifier []type(不需要說明長度)。一個(gè)切片在未初始化之前默認(rèn)為 nil,長度為 0。
切片的初始化格式是:
var slice1 []type = arr1[start:end]
這表示 slice1 是由數(shù)組 arr1 從 start 索引到 end-1 索引之間的元素構(gòu)成的子集(切分?jǐn)?shù)組,start:end 被稱為 slice 表達(dá)式)。
切片也可以用類似數(shù)組的方式初始化:
var x = []int{2, 3, 5, 7, 11}
這樣就創(chuàng)建了一個(gè)長度為 5 的數(shù)組并且創(chuàng)建了一個(gè)相關(guān)切片。
當(dāng)相關(guān)數(shù)組還沒有定義時(shí),我們可以使用 make() 函數(shù)來創(chuàng)建一個(gè)切片,同時(shí)創(chuàng)建好相關(guān)數(shù)組:
var slice1 []type = make([]type, len,cap)
也可以簡寫為 slice1 := make([]type, len),這里 len 是數(shù)組的長度并且也是 slice 的初始長度。cap是容量,其中 cap 是可選參數(shù)。
v := make([]int, 10, 50)
這樣分配一個(gè)有 50 個(gè) int 值的數(shù)組,并且創(chuàng)建了一個(gè)長度為 10,容量為 50 的 切片 v,該切片指向數(shù)組的前 10 個(gè)元素。
以上我們列舉了三種切片初始化方式,這三種方式都比較常用。
如果從數(shù)組或者切片中生成一個(gè)新的切片,我們可以使用下面的表達(dá)式:
a[low : high : max] max-low的結(jié)果表示容量,high-low的結(jié)果表示長度。
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
這里t的容量(capacity)是5-1=4 ,長度是2。
如果切片取值時(shí)索引值大于長度會(huì)導(dǎo)致panic錯(cuò)誤發(fā)生,即使容量遠(yuǎn)遠(yuǎn)大于長度也沒有用,如下面代碼所示:
package main
import "fmt"
func main() {
sli := make([]int, 5, 10)
fmt.Printf("切片sli長度和容量:%d, %d\n", len(sli), cap(sli))
fmt.Println(sli)
newsli := sli[:cap(sli)]
fmt.Println(newsli)
var x = []int{2, 3, 5, 7, 11}
fmt.Printf("切片x長度和容量:%d, %d\n", len(x), cap(x))
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5] // a[low : high : max] max-low的結(jié)果表示容量 high-low為長度
fmt.Printf("切片t長度和容量:%d, %d\n", len(t), cap(t))
// fmt.Println(t[2]) // panic ,索引不能超過切片的長度
}
程序輸出:
切片sli長度和容量:5, 10
[0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0]
切片x長度和容量:5, 5
切片t長度和容量:2, 4
12.2 切片重組(reslice)
slice1 := make([]type, start_length, capacity)
通過改變切片長度得到新切片的過程稱之為切片重組 reslicing,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即長度)。
當(dāng)我們?cè)谝粋€(gè)slice基礎(chǔ)上重新劃分一個(gè)slice時(shí),新的slice會(huì)繼續(xù)引用原有slice的數(shù)組。如果你忘了這個(gè)行為的話,在你的應(yīng)用分配大量臨時(shí)的slice用于創(chuàng)建新的slice來引用原有數(shù)據(jù)的一小部分時(shí),會(huì)導(dǎo)致難以預(yù)期的內(nèi)存使用。
package main
import "fmt"
func get() []byte {
raw := make([]byte, 10000)
fmt.Println(len(raw), cap(raw), &raw[0]) // 顯示: 10000 10000 數(shù)組首字節(jié)地址
return raw[:3] // 10000個(gè)字節(jié)實(shí)際只需要引用3個(gè),其他空間浪費(fèi)
}
func main() {
data := get()
fmt.Println(len(data), cap(data), &data[0]) // 顯示: 3 10000 數(shù)組首字節(jié)地址
}
為了避免這個(gè)陷阱,我們需要從臨時(shí)的slice中使用內(nèi)置函數(shù)copy(),拷貝數(shù)據(jù)(而不是重新劃分slice)到新切片。
package main
import "fmt"
func get() []byte {
raw := make([]byte, 10000)
fmt.Println(len(raw), cap(raw), &raw[0]) // 顯示: 10000 10000 數(shù)組首字節(jié)地址
res := make([]byte, 3)
copy(res, raw[:3]) // 利用copy 函數(shù)復(fù)制,raw 可被GC釋放
return res
}
func main() {
data := get()
fmt.Println(len(data), cap(data), &data[0]) // 顯示: 3 3 數(shù)組首字節(jié)地址
}
程序輸出:
10000 10000 0xc000086000
3 3 0xc000050098
append()內(nèi)置函數(shù):
func append(s S, x ...T) S // T是S元素類型
Append()函數(shù)將 0 個(gè)或多個(gè)具有相同類型 S 的元素追加到切片s后面并且返回新的切片;追加的元素必須和原切片的元素同類型。如果 s 的容量不足以存儲(chǔ)新增元素,append 會(huì)分配新的切片來保證已有切片元素和新增元素的存儲(chǔ)。
因此,append()函數(shù)返回的切片可能已經(jīng)指向一個(gè)不同的相關(guān)數(shù)組了。append()函數(shù)總是返回成功,除非系統(tǒng)內(nèi)存耗盡了。
s0 := []int{0, 0}
s1 := append(s0, 2) // append 單個(gè)元素 s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7) // append 多個(gè)元素 s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...) // append 一個(gè)切片 s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...) // append 切片片段 s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}
append()函數(shù)操作如果導(dǎo)致分配新的切片來保證已有切片元素和新增元素的存儲(chǔ),也就是返回的切片可能已經(jīng)指向一個(gè)不同的相關(guān)數(shù)組了,那么新的slice已經(jīng)和原來slice沒有任何關(guān)系,即使修改了數(shù)據(jù)也不會(huì)同步。
append()函數(shù)操作后,有沒有生成新的slice需要看原有slice的容量是否足夠。
12.3 陳舊的切片(Stale Slices)
多個(gè)slice可以引用同一個(gè)底層數(shù)組。在某些情況下,在一個(gè)slice中添加新的數(shù)據(jù),在原有數(shù)組無法保持更多新的數(shù)據(jù)時(shí),將導(dǎo)致分配一個(gè)新的數(shù)組。而現(xiàn)在其他的slice還指向老的數(shù)組(和老的數(shù)據(jù))。
上一節(jié)我們也說了:append()函數(shù)操作后,有沒有生成新的slice需要看原有slice的容量是否足夠。
下面,我們看看這個(gè)過程是怎么產(chǎn)生的:
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3}
fmt.Println(len(s1), cap(s1), s1) // 輸出 3 3 [1 2 3]
s2 := s1[1:]
fmt.Println(len(s2), cap(s2), s2) // 輸出 2 2 [2 3]
for i := range s2 {
s2[i] += 20
}
// s2的修改會(huì)影響到數(shù)組數(shù)據(jù),s1輸出新數(shù)據(jù)
fmt.Println(s1) // 輸出 [1 22 23]
fmt.Println(s2) // 輸出 [22 23]
s2 = append(s2, 4) // append s2容量為2,這個(gè)操作導(dǎo)致了slice s2擴(kuò)容,會(huì)生成新的底層數(shù)組。
for i := range s2 {
s2[i] += 10
}
// s1 的數(shù)據(jù)現(xiàn)在是老數(shù)據(jù),而s2擴(kuò)容了,復(fù)制數(shù)據(jù)到了新數(shù)組,他們的底層數(shù)組已經(jīng)不是同一個(gè)了。
fmt.Println(len(s1), cap(s1), s1) // 輸出3 3 [1 22 23]
fmt.Println(len(s2), cap(s2), s2) // 輸出3 4 [32 33 14]
}
程序輸出:
3 3 [1 2 3]
2 2 [2 3]
[1 22 23]
[22 23]
3 3 [1 22 23]
3 4 [32 33 14]
本書《Go語言四十二章經(jīng)》內(nèi)容在github上同步地址:https://github.com/ffhelicopter/Go42
本書《Go語言四十二章經(jīng)》內(nèi)容在簡書同步地址: http://m.itdecent.cn/nb/29056963雖然本書中例子都經(jīng)過實(shí)際運(yùn)行,但難免出現(xiàn)錯(cuò)誤和不足之處,煩請(qǐng)您指出;如有建議也歡迎交流。
聯(lián)系郵箱:roteman@163.com