本文總結了Go語言中切片的一些使用技巧和在實際使用中可能會踩「坑」的地方
上篇文章回顧: bats-Bash自動化測試工具
Go語言中的切片html
切片(slice)是Go語言中最基本和最經常使用的數據結構之一,在本文中但願能夠幫助讀者更好的使用這一數據結構。git
切片表示一個具備相同數據類型元素的的序列,切片的長度可變,一般寫成[]T,其中元素的類型都是T。github
切片用來訪問數組的部分或所有元素,這個數組稱爲切片的底層數組。切片主要有三個屬性:指針、長度和容量,指針指向切片的第一個元素,長度是指切片中元素的大小,而容量是指切片第一個元素到底層數組的最後一個元素間元素的個數。golang
切片的操做主要經過append,copy和切片操做符(s[i:j],其中 0<i<j<cap(s))來完成,這裏介紹一下切片經常使用的操做技巧和對數組應用切片操做時須要注意的問題。數組
(1)拼接兩個切片bash
// 拼接切片a和ba = append(a, b...)複製代碼
(2)複製一個切片數據結構
b = append([]T(nil), a...)
b = append(a[:0:0], a...)複製代碼
(3)刪除切片的第i~第j-1個元素([i,j))架構
// 從a中刪除a[i:j]a = append(a[:i], a[j:]...)複製代碼
若是切片的元素是指針或者具備指針成員的結構體,須要避免內存泄露問題,此時須要修改刪除切片元素的代碼以下:app
for k, n := len(a)-j+i, len(a); k < n; k++ {
a[k] = nil // 或該類型的零值}
a = a[:len(a)-j+i]複製代碼
(4)刪除第i個元素運維
// 刪除切片a的第i個元素a = append(a[:i], a[i+1:]...)複製代碼
一樣的,爲了不內存泄露
copy(a[i:], a[i+1:])
a[len(a)-1] = nil // or the zero value of Ta = a[:len(a)-1]複製代碼
(5)彈出切片最後一個元素,即出隊列尾(pop back)
x, a = a[len(a) - 1], a[:len(a)-1]複製代碼
(6)彈出切片第一個元素,即出隊列頭(pop)
x, a = a[0], a[1:]複製代碼
(7)在第i個元素前插入一個切片
// a[:i] 和a[i:]中間插入切片ba = append(a[:i], append(b, a[i:]...)...)複製代碼
(8)切片亂序(Go 1.10以上)
for i := len(a) - 1; i > 0; i-- {
j := rand.Intn(i + 1) // 生成一個[0,i+1)區間內的隨機數
a[i], a[j] = a[j], a[i]
}複製代碼
Go語言的官方wiki上對這些操做有比較詳細的說明,同時也介紹了更多的關於切片的操做,讀者能夠深刻閱讀學習。
首先簡單介紹一下「可尋址性」,簡單來講「可尋址性」是指若是一個對象能夠應用取地址操做符&,那麼這個對象就能夠認爲是可尋址的。
在使用切片的時候,對於數組、指向數組的指針或者切片s, 表達式s[low:high]構造了一個新的切片。不過常常會被忽略的一點是,若是對一個數組進行切片操做,這個數組必須是可尋址的,對於指向數組的指針或切片進行切片操做,則沒有"可尋址性"的要求。
舉例以下:
a := [2]int{1,2}[:] // error,不能對不可尋址的數組進行切片操做。//output: invalid operation [2]int literal[:] (slice of unaddressable value)/* 對指向數組的指針進行切片操做 */func test() *[2]int{ return &[2]int{1,2}
}
b := test()[:] // succeed,能夠對指向數組的指針進行切片操做/* 對切片進行切片操做 */func testSlice() []int { return []int{1,2}
}
d := testSlice()[:] // succeed, 能夠對切片進行切片操做。複製代碼
切片是一種引用類型,在64位架構的機器上,一個切片須要24個字節的內存:指針字段、長度字段和容量字段分別須要8字節,所以在函數中直接傳遞一個切片變量效率是很是高的,可是也正由於切片是引用類型,當函數使用切片做爲形參變量的時候,函數內變量的改變可能會影響到函數外變量的值,好比下面這個例子:
func main() {
s1 := []string{"A", "B", "C"}
fmt.Printf("before foo function, s1 is \t%v\n", s1)
foo(s1)
fmt.Printf("after foo function, s1 is \t%v", s1)
}func foo(s []string) {
s[0] = "New"}複製代碼
輸出爲:
before foo function, s1 is [A B C]
after foo function, s1 is [New B C]複製代碼
能夠看到,函數foo中對切片s1的修改,確實影響到了函數外s1的值。可是在另一些狀況下,函數內對切片變量的改變卻不會影響函數外的切片變量,仍是看一個例子:
func main() {
s1 := []string{"A", "B", "C"}
fmt.Printf("before foo function, s1 is \t%v\n", s1)
foo(s1)
fmt.Printf("after foo function, s1 is \t%v", s1)——
}func foo(s []string) {
s = append(s, "New")
}複製代碼
輸出爲:
before foo function, s1 is [A B C]
after foo function, s1 is [A B C]複製代碼
s1的值雖然在函數中改變,可是在函數外s1的值卻沒有變化。
那麼,在函數中傳遞切片變量的時候,何時會影響外部變量,何時不會影響外部變量呢?其實能夠這樣理解:切片的標頭值是一個指向底層數組的指針,當切片做爲實參傳遞到函數中的時候,這個指針的值會複製給函數中的形參,即函數的實參和形參是共享同一個底層數組的,所以只要在函數中涉及到對底層數組值的修改,都會影響到函數外切片的值。
再舉一個例子以下:
func main() {
arr := [5]string{"A", "B", "C", "D", "E"}
s1 := arr[0:4]
s2 := arr[2:4]
fmt.Printf("before foo function, s2 is \t%v\n", s2)
foo(s1)
fmt.Printf("after foo function, s2 is \t%v", s2)
}func foo(s []string) {
s[2] = "NEW"}複製代碼
在這個例子裏面,s1,s2 共享同一個底層數組,在foo()函數中,咱們仍然修改s1的一個值,能夠看到輸出以下:
before foo function, s2 is [C D]
after foo function, s2 is [NEW D]複製代碼
s2的值由於s1對底層數組的修改,自身的值也被改變了。
在函數中傳遞切片變量的時候,若是函數經過切片修改了底層數組的值,那麼函數外指向該底層數組的切片的值也會被改變,在Go中向函數傳遞切片變量的時候,須要特別注意這一點。
事實上,在Go語言中,全部的引用類型(切片、字典、通道、接口和函數類型),其標頭值都包含一個指向底層數組的指針,所以經過複製來傳遞引用類型的值的副本,本質上就是在共享底層數據結構。
《Go程序設計語言》
《Go語言實戰》
The Go Programming Language Specification- Address operators