# 3.2 切片
在go中,你一般很少直接使用數組。相反,你會使用切片。切片是一個輕量級的結構體封裝,這個結構體被封裝后,代表一個數組的一部分。這里指出了一些創建切片的方式,并指出我們以后會在何時使用這些方式。
第一種方式和我們創建一個數組時有點細微的變化:
`scores := []int{1,4,293,4,9}`
和聲明數組不同的是,聲明切片不需要在方括號中指定其大小。理解2中不同的方式創建切片,下面使用另外一種方式創建切片,這里使用`make`:
`scores := make([]int, 10)`
我們使用`make`,沒有使用`new`,是因為創建一個切片不僅僅是分配一段內存(`new`只能分配一段內存)。需要特別指出的是,我們需要為底層數組分配內存,并且也要初始化這個切片。在上面的例子中,我們初始化了一個長度和容量都是10的切片。長度表示切片的長度,容量表示底層數組的大小。在使用`make`創建切片時,我們可以分別的指定長度和容量大小:
`scores := make([]int, 0, 10)`
上面創建了一個長度為0但是容量為10的切片。(如果你留意過,你會發現`make`和`len`實現了重載。go語言的一些特性會讓你有點失望,因為有些特性是沒有暴露出來給開發者使用。)
要更好的理解切片長度和容量之間的相互作用,讓我們看下面的例子:
```go
func main() {
scores := make([]int, 0, 10)
scores[5] = 9033
fmt.Println(scores)
}
```
上面的例子不能運行。為什么?因為我們創建的切片長度是0。是的,這個底層的數組有10個元素。但是為了訪問切片元素,我們需要明確的擴展切片。一種擴展切片的方式是使用`append`:
```go
func main() {
scores := make([]int, 0, 10)
scores = append(scores, 5)
fmt.Println(scores) // 打印:[5]
}
```
但是這改變為了我們之前代碼的意圖。當往一個長度為0的切片上添加元素時,這個元素會被賦值給切片的第一個元素。不管出于什么原因,那段不能運行的代碼想給切片的第6個元素賦值。為了到達這個目的,我們可以再切分一直我們的切片:
```go
func main() {
scores := make([]int, 0, 10)
scores = scores[0:6]
scores[5] = 9033
fmt.Println(scores)
}
```
調整切片的大小上限是多少?這個上限就是切片的容量大小,在上面的例子中,上限是10。你也許會認為,這實際沒有解決定長數組的問題。事實證明,`append`比較特殊。如果底層的數組已經達到上限,`append`會重新創建一個更大的數組,并將所有的值復制過去(這就是動態數組的工作原理,例如php、python、ruby和javascript等等)。這就是為什么我們在上面的例子中使用`append`。我們必須將`append`返回的值重新賦予給`scores`變量:如果原始切片沒有更多的空間時,`append`可能會創建一個新值。
如果我告訴你go擴展數組使用的是2倍算法(2x algorithm)。你能猜出下面代碼將輸出什么嗎?
```go
func main() {
scores := make([]int, 0, 5)
c := cap(scores)
fmt.Println(c)
for i := 0; i < 25; i++ {
scores = append(scores, i)
// 如果容量已經改變,go為了容下這些新數據,不得不增長數組的長度
if cap(scores) != c {
c = cap(scores)
fmt.Println(c)
}
}
}
```
切片`scores`的初始容量是5。但是為了容納20個元素,切片的容量必須擴展3次,分別是10、20和40。
作為最后一個例子,思考一下:
```go
func main() {
scores := make([]int, 5)
scores = append(scores, 9332)
fmt.Println(scores)
}
```
當面的代碼輸出是`[0, 0, 0, 0, 0, 9332]`。也許你認為應該是`[9332, 0, 0, 0, 0]`。對人類來說,這似乎更合乎邏輯。但是對于編譯器來說,你是要告訴它往一個已經擁有5個元素的切片添加一個值。
最后,這里提供了4種常用的方式去初始化一個切片:
```go
names := []string{"leto", "jessica", "paul"}
checks := make([]bool, 10)
var names []string
scores := make([]int, 0, 20)
```
你該使用哪一個?第一種你不需要太多的說明。但是使用這種方式你得提前知道你想往數組存放的值。
第二種方式在你想往切片的特定位置寫入一個值時很有用,例如:
```go
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, len(saiyans))
for index, saiyan := range saiyans {
powers[index] = saiyan.Power
}
return powers
}
```
第三種方式會返回一個空切片,一般和`append`一起使用,此時切片的元素數量是未知的。
最后一種方式可以讓我們指定切片的初始容量。當我們大概知道需要多少元素時很有用。即使你知道元素的個數,`append`也能被使用,這主要取決于個人喜好:
```go
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, 0, len(saiyans))
for _, saiyan := range saiyans {
powers = append(powers, saiyan.Power)
}
return powers
}
```
切片作為一個數組的封裝是一個非常有用的概念。很多語言都有類似的概念。javascript和ruby的數組都有一個`slice`方法。你也可以在ruby中通過`[START..END]`得到一個切片,或者在python中通過`[START:END]`得到一個切片。然而,在一些語言中,切片確實是從原始數組拷貝而來的新數組。如果我們使用`ruby`,下面代碼將輸出什么?
```go
scores = [1,2,3,4,5]
slice = scores[2..4]
slice[0] = 999
puts scores
```
答案是`[1, 2, 3, 4, 5]`。因為切片`slice`是由值拷貝組成的一個全新數組。現在,同等情況下看go:
```go
scores := []int{1,2,3,4,5}
slice := scores[2:4]
slice[0] = 999
fmt.Println(scores)
```
輸出是`[1, 2, 999, 4, 5]`。
這會如何改變你的代碼。例如,很多函數需要一個位置參數。在javascript中,如你我們想在前五個字符后查找一個字符串中的第一個空白符(對,切片也當字符串處理),我們可以這樣寫:
```javascript
haystack = "the spice must flow";
console.log(haystack.indexOf(" ", 5));
```
在go中,我們使用切片:
`strings.Index(haystack[5:], " ")`
從上面例子中,我們可以看出`[X:]`表示`X`到結尾的一種縮寫。而`[:X]`是開始到`X`的一種縮寫。不像其他語言,go不支持負值索引。如果我們想要切片所有元素,但除了最后一個,我們可以這樣寫:
```go
scores := []int{1, 2, 3, 4, 5}
scores = scores[:len(scores)-1]
```
上述是一種從一個亂序的切片中去除一個值的有效方法。
```go
func main() {
scores := []int{1, 2, 3, 4, 5}
scores = removeAtIndex(scores, 2)
fmt.Println(scores)
}
func removeAtIndex(source []int, index int) []int {
lastIndex := len(source) - 1
//swap the last value and the value we want to remove
source[index], source[lastIndex] = source[lastIndex], source[index]
return source[:lastIndex]
}
```
最后,我們已經學習了切片,我們來學習一下另外一個常用的內置函數:`copy`。`copy`是眾多函數中重點顯示出切片如何改變我們代碼方式的函數之一。正常情況下,拷貝一個數組到另外一個數組的方法需要5個參數:`source`,`sourceStart`,`count`,`destination`和`destinationStart`。但是在切片中,我們只需要2個參數:
```go
import (
"fmt"
"math/rand"
"sort"
)
func main() {
scores := make([]int, 100)
for i := 0; i < 100; i++ {
scores[i] = int(rand.Int31n(1000))
}
sort.Ints(scores)
worst := make([]int, 5)
copy(worst, scores[:5])
fmt.Println(worst)
}
```
花點時間研究上面的代碼。試著改變一些代碼。如果你使用`copy(worst[2:4], scores[:5])`方式去復制看看會發生什么,或者試著復制多于或者少于5個值到`worst`。
## 鏈接
- 關于本書
- 引言
- 準備工作
- 安裝開發環境
- 開始使用Go
- 創建一個Go模塊
- 第1章:基礎知識
- 1.1 編譯
- 1.2 靜態類型
- 1.3 類c語法
- 1.4 垃圾回收
- 1.5 運行go代碼
- 1.6 導入包
- 1.7 變量和聲明
- 1.8 函數聲明
- 1.9 繼續之前
- 第2章:語法學習
- 2.1 聲明和初始化
- 2.2 結構體上的函數
- 2.3 構造函數
- 2.4 new
- 2.5 結構體字段
- 2.6 組合
- 2.7 指針類型和值類型
- 2.8 繼續之前
- 第3章:復雜類型
- 3.1 數組
- 3.2 切片
- 3.3 映射
- 3.4 指針類型和值類型
- 3.5 繼續之前
- 第4章:面向對象
- 4.1 包
- 4.2 接口
- 4.3 繼續之前
- 第5章:綜合知識
- 5.1 錯誤處理
- 5.2 defer
- 5.3 go語言風格
- 5.4 初始化的if
- 5.5 空接口和轉換
- 5.6 字符串和字節數組
- 5.7 函數類型
- 5.8 內存分配
- 第6章:高并發
- 6.1 go協程
- 6.2 同步
- 6.3 通道
- 6.4 繼續之前
- 第7章:工具庫
- 7.1 類型轉換
- 7.2 時間操作
- 第8章:程序測試
- 單元測試
- 性能測試
- 第9章:簡單實例
- 內存分配
- 第10章:項目實戰
- 結論
- 附錄