golang中的slice操作
本文總結了Go語言中切片的一些使用技巧和在實際使用中可能會踩“坑”的地方
上篇文章回顧:bats-Bash自動化測試工具
Go語言中的切片
切片(slice)是Go語言中最基本和最常用的資料結構之一,在本文中希望可以幫助讀者更好的使用這一資料結構。
01什麼是切片
切片表示一個具有相同資料型別元素的的序列,切片的長度可變,通常寫成[]T,其中元素的型別都是T。
切片用來訪問陣列的部分或全部元素,這個陣列稱為切片的底層陣列。切片主要有三個屬性:指標、長度和容量,指標指向切片的第一個元素,長度是指切片中元素的大小,而容量是指切片第一個元素到底層陣列的最後一個元素間元素的個數。
02切片的一些操作
切片的操作主要通過append,copy和切片操作符(s[i:j],其中 0<i<j<cap(s))來完成,這裡介紹一下切片常用的操作技巧和對陣列應用切片操作時需要注意的問題。
1、切片常用操作技巧
(1)拼接兩個切片
// 拼接切片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:]...)複製程式碼
如果切片的元素是指標或者具有指標成員的結構體,需要避免記憶體洩露問題,此時需要修改刪除切片元素的程式碼如下:
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 上對這些操作有比較詳細的說明,同時也介紹了更多的關於切片的操作,讀者可以深入閱讀學習。
2、切片操作符合Go語言中的可訊址性
首先簡單介紹一下“可定址性”,簡單來說“可定址性”是指如果一個物件可以應用取地址操作符&,那麼這個物件就可以認為是可定址的。
在使用切片的時候,對於陣列、指向陣列的指標或者切片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, 可以對切片進行切片操作。複製程式碼
03切片作為引數在函式中傳遞
切片是一種引用型別,在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
。