---
# System prepended metadata

title: Go 語言的陣列，搭配迴圈輕鬆省事
tags: [Google, Go, 程式設計, Golang]

---

# 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` `程式設計`
