本篇文章有不少程式碼,如果這裡的排版讓你感到閱讀困難,請服用 HackMD 好讀版。
說到利用電腦程式來把處理資料的流程自動化,通常都會遇上要處理很多相同性質的資料,那自然是少不了陣列(array)的蹤影。
比如說,我們現在有十個數:
4, 16, 17, 23, 28, 32, 47, 129, 231, 477
想要把這裡面的質數挑出來,自然是先把這些數裝到陣列裡面,同時寫個 isPrime() 函式來負責判斷一個數是不是質數,然後把整個陣列 run 過一次,一個一個丟進去檢查。
這時候問題就來了:在 Golang 裡面,我們要怎麼使用陣列?
? Golang 的陣列(array)
在 C 語言裡面,我們知道 array 就相當於一次宣告好固定數量的多個變數,例如:
int myArray[5] = {4, 6, 8, 10, 12}; for (int i=0; i<5; i++) { printf("myArray[%d] = %d\n", i, myArray[i]); } |
在 Golang 裡面也是一樣,只是 Go 語言的陣列是把數量和資料型態寫在一起:
var myArray = [5]int{4, 6, 8, 10, 12} for i := 0; i < 5; i++ { fmt.Printf("myArray[%d] = %d\n", i, myArray[i]) } |
如果我們賦值的時候懶得去細數元素數量呢?我們可以這麼做:
var myArray = [...]int{4, 6, 8, 10, 12} |
宣告變數的時候使用 […],編譯器就會自動幫你判斷出是 [5]int 型態的 array 了,因為你在賦值的時候填了五個數值進去。如果今天我們不想對陣列本身賦值,但想先指定好大小,我們有兩種寫法:
var myArray [5]int |
var myArray = [5]int{} |
這兩種寫法造成的結果相同,myArray 的內容都會自動被設為 {0, 0, 0, 0, 0},只是前者的做法是「宣告一個 [5]int 型態的變數,但不去指定內容」,後者的做法則是「宣告一個變數,然後創造一個 [5]int 型態的空 array,再賦值進這個變數」
所以,[5]int 表示的是「型態」,而 [5]int{} 表示的是「一個 array 的實體」,加上大括號意義是不一樣的,千萬不要搞混了。
就像剛才提到的,Golang 是把元素數量和元素的型態綁在一起,所以 [5]int 和 [8]int 是無法直接互通的東西。如果你嘗試這麼做:
func main() { var myArray = [...]int{4, 6, 8, 10, 12} printArrayContent(myArray) } func printArrayContent(myArray [8]int) { for i := 0; i < 8; i++ { fmt.Println(i, myArray[i]) } } |
你會得到這樣的錯誤訊息:
cannot use myArray (variable of type [5]int) as [8]int value in argument to printArrayContent
簡單來說就是「不能把一個 [5]int 型態的變數直接拿來當成 [8]int 型態的變數使用」。
除此之外,只要你宣告了它是 [5]int,那它就永遠不能夠存放超過五個元素,你也不能試圖存取 myArray[6],否則會產生 index out of range 的錯誤,導致程式進入 panic。
這麼說來,如果我們只使用 array 的話,其實非常不方便——特別是當我們無法確定陣列裡究竟有多少元素的時候。
? Golang 的切片(slice)
於是切片(slice)誕生了。顧名思義,它就是從 array 切下來的片段,而且長度是動態的,不受限於元素數量。我們用白話來說,[]int、[5]int、[8]int 都是不一樣的東西。
因為使用固定長度的 array 來處理資料很不方便,所以實務上我們多會直接用 []int 來處理。從 Golang 的底層來看,其實 slice 的背後仍然是 array,只是比 array 還要多實現了更多的功能,是一種 array 的延伸。
如果想要產生一個 slice,你可以直接宣告而成,只要中括號裡面不填任何東西就好了:
mySlice := []int{37, 43, 57, 66, 99, 125} |
也可以從 array 切出一段 slice:
myArray := [...]int{37, 43, 57, 66, 99, 125} mySlice := myArray[2:5] // get a []int instead of [3]int fmt.Println(mySlice) // -> [57 66 99] |
一組含有 6 個元素的 array(或 slice),切割的時候我們的 index 從 0 開始算起,一直到 6 結束。也因此,我們求 myArray[2:5] 會得到一個 []int{57, 66, 99},當然你也可以省略尾端或起點:
myArray := [...]int{37, 43, 57, 66, 99, 125} mySlice := myArray[3:] // same as myArray[3:6] fmt.Println(mySlice) // -> [66 99 125] |
myArray := [...]int{37, 43, 57, 66, 99, 125} mySlice := myArray[:2] // same as myArray[0:2] fmt.Println(mySlice) // -> [37 43] |
如果起點跟終點都省略了,那就相當於是從頭到尾都完整地複製一次,一樣會是得到一個 slice,所以我們如果有一個 array 叫做 myArray,只要用 myArray[:] 就可以把這個 myArray 轉換成一個 slice:
myArray := [...]int{37, 43, 57, 66, 99, 125} fmt.Println(myArray[:]) // get a []int instead of [6]int |
當然你也可以從 slice 切出 slice 來:
mySlice := []int{37, 43, 57, 66, 99, 125} fmt.Println(mySlice[2:5]) // -> [57 66 99] |
如果你手上有一個 string,一樣也可以利用上面這種 [start:end] 的句法來切割它,切出來的東西仍然會是一個 string:
myString := "ABCDEFGHIJKLMN" fmt.Println(myString[3:7]) // -> DEFG |
? 操作切片的元素增減
一般我們會直接使用 append() 來產生一個新的 slice,把舊的 slice 和新的元素拼接而成:
mySlice := []int{555, 666, 777} mySlice = append(mySlice, 8899) fmt.Println(mySlice) // -> [555 666 777 8899] |
因為 append 函式的自變數數量是浮動的,所以在 slice 後面緊跟的元素可以只填一個,也可以兩個以上:
mySlice := []int{555, 666, 777} mySlice = append(mySlice, 8899, 7788, 5566) fmt.Println(mySlice) // -> [555 666 777 8899 7788 5566] |
要注意,append 函式僅限於「slice 和元素的拼接」,如果你想要把兩個 slice 拼接起來,第二個 slice 就必須使用 … 的後綴符號來展開:
mySliceA := []int{555, 666, 777} mySliceB := []int{8899, 7788, 5566} mySliceA = append(mySliceA, mySliceB...) fmt.Println(mySliceA) // -> [555 666 777 8899 7788 5566] |
如果要從一個 slice 當中剔除 index=2(也就是第三個)的元素,我們可以分成兩個切片再拼接起來:
mySlice := []int{55, 66, 77, 88, 99} mySlice = append(mySlice[:2], mySlice[3:]...) fmt.Println(mySlice) // -> [55 66 88 99] |
? 陣列或切片的迭代
瞭解 array 和 slice 的差異之後,接下來最重要的當屬迭代(iteration)。當我們想要用迭代的方式巡完整個陣列或切片,一般的做法是先取得元素數量,然後用 for 迴圈去處理它,例如:
mySlice := []int{55, 66, 77, 88, 99} // you can get the length of slice with len() for i := 0; i < len(mySlice); i++ { fmt.Println(mySlice[i]) } |
利用一般的 for 迴圈可以達到效果(就像在 C 語言一樣),但有的場合我們希望寫出來的程式碼更接近 for-each 的語意,並不在乎這個 slice 到底裝了確切多少個元素,那就會直接使用 range 關鍵字來針對整個 slice 循環一次:
mySlice := []int{55, 66, 77, 88, 99} for i := range mySlice { fmt.Println(mySlice[i]) } |
這迴圈裡的 i 就會依序是 0, 1, 2, 3, 4。如果同時用兩個變數來接收 range 每次回傳的值,則會收到 index 和 value:
mySlice := []int{55, 66, 77, 88, 99} for i, v := range mySlice { fmt.Println(i, v) // try it! } |
有的場合我們只需要 value 而不需要 index,那就利用底線把回傳的 index 拋掉:
mySlice := []int{55, 66, 77, 88, 99} for _, v := range mySlice { fmt.Println(v) } |
能宣告、能切割、能增減、能遍歷,這些基本上就是 slice 的常用功能了。
? slice 只能裝基本型態嗎?
當然不是。無論是基本型態、結構體或者是空介面(interface{})都是支援 slice 的。除此之外,slice 也可以裝 slice,也就是平常在其他語言的多維陣列。
既然 []int 可以放很多個 int,那麼 [][]int 就可以放很多個 []int 了:
mySlice2D := [][]int{ []int{1, 2, 3, 4, 5}, []int{2, 4, 6, 8, 10}, []int{3, 6, 9, 12, 15}, } for i := range mySlice2D { for j := range mySlice2D[i] { fmt.Printf("%d,", mySlice2D[i][j]) } fmt.Println("----") } |
因為 [][]int 裡面裝的一定都會是 []int,所以我們可以把裡面的 []int 省略,而 range 的環節同樣也可以用 value 的方式來接收:
mySlice2D := [][]int{ {1, 2, 3, 4, 5}, {2, 4, 6, 8, 10}, {3, 6, 9, 12, 15}, } for _, s := range mySlice2D { for _, v := range s { fmt.Printf("%d,", v) } fmt.Println("----") } |
至於結構體和空介面的 slice,就留待下次講結構體主題的時候再一起說吧。
? array 和 slice 用來傳值時產生的差異
這兩者的差異,不只體現在動態或靜態的元素數量。我們前面曾經提到,slice 本身就是一種 array 的延伸,其實 slice 的結構包含了一個指向底層 array 的指標(只是它封裝得很好,讓你覺得 slice 是一種獨立於 array 的東西)。
如果我們把 slice 傳到函式裡面,那就相當於把底層 array 的指標傳進去,也因此如果把 slice 傳進去修改數值,我們會發現這個更動在函式之外也是有效的:
func main() { mySlice := []int{1, 2, 3, 4, 5} fmt.Println(mySlice) // -> [1 2 3 4 5] changeValue(mySlice) fmt.Println(mySlice) // -> [1 2 99 4 5] } func changeValue(s []int) { if len(s) < 3 { return } s[2] = 999 } |
而當我們傳遞給函式的是 array 而非 slice 時,Golang 會直接把整個 array 複製一份到函式裡面,即使在函式裡修改了 array,原先外面的 array 也不會受影響:
func main() { mySlice := [5]int{1, 2, 3, 4, 5} fmt.Println(mySlice) // -> [1 2 3 4 5] changeValue(mySlice) fmt.Println(mySlice) // -> [1 2 3 4 5] } func changeValue(s [5]int) { s[2] = 999 } |
這就是 slice 跟 array 的差別了。原則上我們平常使用還是以 slice 為主,至少我平常在寫 Go 的時候幾乎沒用過固定長度的 array 就是。有的時候我們可能會把 slice 作為參數傳遞給函式使用,就要特別注意 slice 傳進去的會是指標,以避免遇到預期之外的 bug。
唉,slice 這麼好用,我真的不會想寫 C 了。
HackMD 好讀版:https://hackmd.io/@upk1997/go-slice
縮圖素材原作者:Renée French(CC BY-SA 3.0)