# Go 語言的陣列,搭配迴圈輕鬆省事
說到利用電腦程式來把處理資料的流程自動化,通常都會遇上要處理很多相同性質的資料,那自然是少不了陣列(array)的蹤影。
比如說,我們現在有十個數:
4, 16, 17, 23, 28, 32, 47, 129, 231, 477
想要把這裡面的質數挑出來,自然是先把這些數裝到陣列裡面,同時寫個 isPrime() 函式來負責判斷一個數是不是質數,然後把整個陣列 run 過一次,一個一個丟進去檢查。
這時候問題就來了:在 Golang 裡面,我們要怎麼使用陣列?
## Golang 的陣列(array)
在 C 語言裡面,我們知道 array 就相當於一次宣告好固定數量的多個變數,例如:
```c
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 語言的陣列是把數量和資料型態寫在一起:
```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])
}
```
如果我們賦值的時候懶得去細數元素數量呢?我們可以這麼做:
```go
var myArray = [...]int{4, 6, 8, 10, 12}
```
宣告變數的時候使用 [...],編譯器就會自動幫你判斷出是 [5]int 型態的 array 了,因為你在賦值的時候填了五個數值進去。如果今天我們不想對陣列本身賦值,但想先指定好大小,我們有兩種寫法:
```go
var myArray [5]int
```
```go
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 是無法直接互通的東西。如果你嘗試這麼做:
```go
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,你可以直接宣告而成,只要中括號裡面不填任何東西就好了:
```go
mySlice := []int{37, 43, 57, 66, 99, 125}
```
也可以從 array 切出一段 slice:
```go
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]
```
![](https://i.imgur.com/BBvXVLM.png)
一組含有 6 個元素的 array(或 slice),切割的時候我們的 index 從 0 開始算起,一直到 6 結束。也因此,我們求 myArray[2:5] 會得到一個 []int{57, 66, 99},當然你也可以省略尾端或起點:
```go
myArray := [...]int{37, 43, 57, 66, 99, 125}
mySlice := myArray[3:] // same as myArray[3:6]
fmt.Println(mySlice) // -> [66 99 125]
```
```go
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:
```go
myArray := [...]int{37, 43, 57, 66, 99, 125}
fmt.Println(myArray[:]) // get a []int instead of [6]int
```
當然你也可以從 slice 切出 slice 來:
```go
mySlice := []int{37, 43, 57, 66, 99, 125}
fmt.Println(mySlice[2:5]) // -> [57 66 99]
```
如果你手上有一個 string,一樣也可以利用上面這種 [start:end] 的句法來切割它,切出來的東西仍然會是一個 string:
```go
myString := "ABCDEFGHIJKLMN"
fmt.Println(myString[3:7]) // -> DEFG
```
## 操作切片的元素增減
一般我們會直接使用 append() 來產生一個新的 slice,把舊的 slice 和新的元素拼接而成:
```go
mySlice := []int{555, 666, 777}
mySlice = append(mySlice, 8899)
fmt.Println(mySlice) // -> [555 666 777 8899]
```
因為 append 函式的自變數數量是浮動的,所以在 slice 後面緊跟的元素可以只填一個,也可以兩個以上:
```go
mySlice := []int{555, 666, 777}
mySlice = append(mySlice, 8899, 7788, 5566)
fmt.Println(mySlice) // -> [555 666 777 8899 7788 5566]
```
要注意,append 函式僅限於「slice 和元素的拼接」,如果你想要把兩個 slice 拼接起來,第二個 slice 就必須使用 ... 的後綴符號來展開:
```go
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(也就是第三個)的元素,我們可以分成兩個切片再拼接起來:
```go
mySlice := []int{55, 66, 77, 88, 99}
mySlice = append(mySlice[:2], mySlice[3:]...)
fmt.Println(mySlice) // -> [55 66 88 99]
```
## 陣列或切片的迭代
瞭解 array 和 slice 的差異之後,接下來最重要的當屬迭代(iteration)。當我們想要用迭代的方式巡完整個陣列或切片,一般的做法是先取得元素數量,然後用 for 迴圈去處理它,例如:
```go
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 循環一次:
```go
mySlice := []int{55, 66, 77, 88, 99}
for i := range mySlice {
fmt.Println(mySlice[i])
}
```
這迴圈裡的 i 就會依序是 0, 1, 2, 3, 4。如果同時用兩個變數來接收 range 每次回傳的值,則會收到 index 和 value:
```go
mySlice := []int{55, 66, 77, 88, 99}
for i, v := range mySlice {
fmt.Println(i, v) // try it!
}
```
有的場合我們只需要 value 而不需要 index,那就利用底線把回傳的 index 拋掉:
```go
mySlice := []int{55, 66, 77, 88, 99}
for _, v := range mySlice {
fmt.Println(v)
}
```
能宣告、能切割、能增減、能遍歷,這些基本上就是 slice 的常用功能了。
## slice 只能裝基本型態嗎?
當然不是。無論是基本型態、結構體或者是空介面(interface{})都是支援 slice 的。除此之外,slice 也可以裝 slice,也就是平常在其他語言的多維陣列。
既然 []int 可以放很多個 int,那麼 [][]int 就可以放很多個 []int 了:
```go
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 的方式來接收:
```go
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 傳進去修改數值,我們會發現這個更動在函式之外也是有效的:
```go
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 也不會受影響:
```go
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 了。
###### tags: `Go` `Golang` `Google` `程式設計`