Golang 學習筆記二 陣列、切片
一、陣列
Go 語言裡面的陣列其實很不常用,這是因為陣列是定長的靜態的,一旦定義好長度就無法更改,而且不同長度的陣列屬於不同的型別,之間不能相互轉換相互賦值,用起來多有不方便之處。
切片是動態的陣列,是可以擴充內容增加長度的陣列。當長度不變時,它用起來就和普通陣列一樣。當長度不同時,它們也屬於相同的型別,之間可以相互賦值。這就決定了陣列的應用領域都廣泛地被切片取代了。
1.只宣告的話,全部是零值
func main() { var a [9]int fmt.Println(a) } ------------ [0 0 0 0 0 0 0 0 0]
三種宣告方式,給初值方式是一樣的
var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9} var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} c := [8]int{1, 2, 3, 4, 5, 6, 7, 8} //[0,10,20,0,0] array := [5]int{1:10,2:20}
2.下標訪問
func main() { var squares [9]int for i := 0; i < len(squares); i++ { squares[i] = (i + 1) * (i + 1) } fmt.Println(squares) } -------------------- [1 4 9 16 25 36 49 64 81]
3.陣列賦值
同樣的子元素型別並且是同樣長度的陣列才可以相互賦值,否則就是不同的陣列型別,不能賦值。陣列的賦值本質上是一種淺拷貝操作,賦值的兩個陣列變數的值不會共享。
func main() { var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9} var b [9]int b = a a[0] = 12345 fmt.Println(a) fmt.Println(b) } -------------------------- [12345 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9]
4.range關鍵字來遍歷
func main() { var a = [5]int{1,2,3,4,5} for index := range a { fmt.Println(index, a[index]) } for index, value := range a { fmt.Println(index, value) } } ------------ 0 1 1 2 2 3 3 4 4 5 0 1 1 2 2 3 3 4 4 5
每次迴圈迭代, range 產生一對值;索引以及在該索引處的元素值。如果不需要索引怎麼辦,range 的語法要求, 要處理元素, 必須處理索引。一種思路是把索引賦值給一個臨時變數,如 temp , 然後忽略它的值,但Go語言不允許使用無用的區域性變數(local variables),因為這會導致編譯錯誤。Go語言中這種情況的解決方法是用 空識別符號 (blank identifier),即 _ (也就是下劃線)。空識別符號可用於任何語法需要變數名但程式邏輯不需要的時候, 例如, 在迴圈裡,丟棄不需要
的迴圈索引, 保留元素值。
for _,value := range s1{ fmt.Println(value) }
5.函式間傳遞陣列
在函式間傳遞變數時,總是以值的方式傳遞。如果變數是個陣列,意味著整個陣列,不管有多長,都會完整複製,並傳遞給函式。有一種更好且更有效的方法來處理這個操作,就是隻傳入指向陣列的指標,只需要複製8個位元組的資料,這樣危險在於,共享了記憶體,會改變原始值。
二、切片

image.png
上圖中一個切片變數包含三個域,分別是底層陣列的指標、切片的長度 length 和切片的容量 capacity。切片支援 append 操作可以將新的內容追加到底層陣列,也就是填充上面的灰色格子。如果格子滿了,切片就需要擴容,底層的陣列就會更換。
形象一點說,切片變數是底層陣列的檢視,底層陣列是臥室,切片變數是臥室的窗戶。通過窗戶我們可以看見底層陣列的一部分或全部。一個臥室可以有多個窗戶,不同的窗戶能看到臥室的不同部分。
1.切片的建立有多種方式,我們先看切片最通用的建立方法,那就是內建的 make 函式
var s1 []int = make([]int, 5, 8) var s2 []int = make([]int, 8) // 滿容切片
make 函式建立切片,需要提供三個引數,分別是切片的型別、切片的長度和容量。其中第三個引數是可選的,如果不提供第三個引數,那麼長度和容量相等,也就是說切片的滿容的。
使用 make 函式建立的切片內容是「零值切片」,也就是內部陣列的元素都是零值。Go 語言還提供了另一個種建立切片的語法,允許我們給它賦初值。 使用這種方式建立的切片是滿容的。
func main() { var s []int = []int{1,2,3,4,5}// 滿容的 fmt.Println(s, len(s), cap(s)) } --------- [1 2 3 4 5] 5 5
這種寫法,和陣列的定義非常相似,注意區別就在方括號裡是否寫了長度。
var i1 [5]int = [5]int{1,2,3,4,5} var i2 []int = []int{1,2,3,4,5} i1 = append(i1, 6) i2 = append(i2,7)
編譯不通過,i1用不了append方法,引數必須是slice
2.切片的賦值
切片的賦值是一次淺拷貝操作,拷貝的是切片變數的三個域,你可以將切片變數看成長度為 3 的 int 型陣列,陣列的賦值就是淺拷貝。拷貝前後兩個變數共享底層陣列,對一個切片的修改會影響另一個切片的內容,這點需要特別注意。
func main() { var s1 = make([]int, 5, 8) // 切片的訪問和陣列差不多 for i := 0; i < len(s1); i++ { s1[i] = i + 1 } var s2 = s1 fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2)) // 嘗試修改切片內容 s2[0] = 255 fmt.Println(s1) fmt.Println(s2) } -------- [1 2 3 4 5] 5 8 [1 2 3 4 5] 5 8 [255 2 3 4 5] [255 2 3 4 5]
3.append追加
相對於陣列,切片可以增加長度。當append返回時,會返回一個包含修改結果的新切片。函式append總會增加新切片的長度,而容量有可能改變,也有可能不改變,這取決於被操作切片的可用容量。
slice := []int{10,20,30,40,50} newSlice := slice[1:3] newSlice = append(newSlice,60);
因為newSlice在底層數組裡還有額外容量可用,append操作將可用元素合併到切片的長度,並對其進行賦值。由於和原始的slice共享同一個底層陣列,slice中索引為3的元素值也被改動了。
如果切片的底層陣列沒有足夠可用容量,append函式會建立一個新的底層陣列,將被引用的現有值複製到新數組裡,再追加新值。
slice := []int{10,20,30,40} newSlice = append(slice,50)
函式append會自動處理底層陣列的容量增長,在切片容量小於1000個元素時,總是會成倍地增加容量。超過1000時,每次增加25%。
append可以在一次呼叫傳遞多個追加值,如果使用...運算子,可以將一個切片所有元素追加到另一個切片裡
s1 := []int{1,2} s2 := []int{3,4} append(s1,s2...);
4.切割
func main() { var s1 = []int{1,2,3,4,5,6,7} // start_index 和 end_index,不包含 end_index // [start_index, end_index) var s2 = s1[2:5] s2[0] = 0 fmt.Println(s1, len(s1), cap(s1)) fmt.Println(s2, len(s2), cap(s2)) } ------------ [1 2 0 4 5 6 7] 7 7 [0 4 5] 3 5

image.png
子切片語法上要提供起始和結束位置,這兩個位置都可選的,不提供起始位置,預設就是從母切片的初始位置開始(不是底層陣列的初始位置),不提供結束位置,預設就結束到母切片尾部(是長度線,不是容量線)。使用過 Python 的同學可能會問,切片支援負數的位置麼,答案是不支援,下標不可以是負數。
對陣列進行切割可以轉換成切片,切片將原陣列作為內部底層陣列。也就是說修改了原陣列會影響到新切片,對切片的修改也會影響到原陣列。
func main() { var a = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} var b = a[2:6] fmt.Println(b) a[4] = 100 fmt.Println(b) } ------- [3 4 5 6] [3 4 100 6]
5.切割的第三個引數
source := []string{"apple","orange","plum","banana","grape"}; slice := source[2:3]//plum1個元素,到結尾,3個容量 slice := source[2:3:4]//也是一個1元素,不過有4-2=2個容量
使用第3個引數將長度和容量保持一致後,再使用append操作就會建立新的底層陣列,從而和原底層陣列分離,這樣就不用擔心影響到其他切片中的資料。
6.內建 copy 函式 func copy(dst, src []T) int
copy 函式不會因為原切片和目標切片的長度問題而額外分配底層陣列的記憶體,它只負責拷貝陣列的內容,從原切片拷貝到目標切片,拷貝的量是原切片和目標切片長度的較小值 —— min(len(src), len(dst)),函式返回的是拷貝的實際長度。
func main() { var s = make([]int, 5, 8) for i:=0;i<len(s);i++ { s[i] = i+1 } fmt.Println(s) var d = make([]int, 2, 6) var n = copy(d, s) fmt.Println(n, d) } ----------- [1 2 3 4 5] 2 [1 2]
7.range遍歷
需要強調的是,range建立了每個元素的副本,而不是直接返回該元素的引用
// 建立一個整型切片 // 其長度和容量都是 4 個元素 slice := []int{10, 20, 30, 40} // 迭代每個元素,並顯示值和地址 for index, value := range slice { fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index]) } Output: Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100 Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104 Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108 Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
因為迭代返回的變數是一個迭代過程中根據切片依次賦值的新變數,所以 value 的地址總是相同的。要想獲取每個元素的地址,可以使用切片變數和索引值。
8.沒有push,pop這些功能
Pop x, a = a[len(a)-1], a[:len(a)-1] Push a = append(a, x) Shift x, a := a[0], a[1:] Unshift a = append([]T{x}, a...)