- 原文地址:Part 11: Arrays and Slices
- 原文作者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
這一章我們將學習 Go 語言中的陣列和切片。
陣列
陣列是屬於同一型別的元素的集合。例如,整數 5, 8, 9, 79, 76 的集合可以構成陣列。Go 中的陣列不允許混合不同型別的值,例如包含字串和整數。
陣列的宣告
陣列的表示為[n] T
。n 表示陣列中元素的數量,T 表示每個元素的型別。元素的數量 n 也是型別的一部分(我們將在稍後更詳細地討論它。)
有多種宣告陣列的方法,我們一個一個來看看,
package main
import (
"fmt"
)
func main() {
var a [3]int //int array with length 3
fmt.Println(a)
}
複製程式碼
Run in playground
var a [3]int
宣告瞭一個長度為 3 的整型陣列,該陣列所有的元素會被自動初始化為陣列型別的零值。在這個例子中陣列的型別為整數,所以 a 陣列所有的元素會被初始化為 0。執行程式碼會輸出,[0 0 0]
陣列的索引從 0 開始到陣列的長度減 1,我們來給陣列 a 賦一些值,
package main
import (
"fmt"
)
func main() {
var a [3]int //int array with length 3
a[0] = 12 // array index starts at 0
a[1] = 78
a[2] = 50
fmt.Println(a)
}
複製程式碼
賦值後,程式將輸出[12 78 50]
我們也可以使用簡短宣告來建立一個陣列。
package main
import (
"fmt"
)
func main() {
a := [3]int{12, 78, 50} // short hand declaration to create array
fmt.Println(a)
}
複製程式碼
該程式和上一個輸出的一樣[12 78 50]
在簡短宣告的時候,不需要為陣列中的所有元素分配值。
package main
import (
"fmt"
)
func main() {
a := [3]int{12}
fmt.Println(a)
}
複製程式碼
上述程式碼的第 8 行a := [3]int{12}
宣告瞭一個長度為 3 的陣列,但是隻提供了一個值 12,剩下的兩個元素將會被零值自動填充,程式將輸出,[12 0 0]
在宣告陣列的時候,甚至可以不指定長度,而用...
替代,該方法編譯器會幫你計算出長度。來看看下面的程式碼,
package main
import (
"fmt"
)
func main() {
a := [...]int{12, 78, 50} // ... makes the compiler determine the length
fmt.Println(a)
}
複製程式碼
陣列的大小是型別的一部分。因此[5] int
和[25] int
是不同的型別。因此,陣列的大小是無法調整的。不要擔心這個問題,因為切片就是為了解決這個問題。
package main
func main() {
a := [3]int{5, 78, 8}
var b [5]int
b = a //not possible since [3]int and [5]int are distinct types
}
複製程式碼
在上述程式的第 6 行,我們嘗試把[3]int
型別的值賦給[5]int
,這是不允許的,所以編譯器將會報錯main.go:6: cannot use a (type [3]int) as type [5]int in assignment.
陣列是值傳遞型別
Go 中的陣列是值傳遞型別而不是引用傳遞型別。這意味著當它們賦值給新變數時,會將原始陣列的副本賦給新變數。如果對新變數進行了更改,則它不會改變原來的陣列。
package main
import "fmt"
func main() {
a := [...]string{"USA", "China", "India", "Germany", "France"}
b := a // a copy of a is assigned to b
b[0] = "Singapore"
fmt.Println("a is ", a)
fmt.Println("b is ", b)
}
複製程式碼
上述程式的第 7 行,把a
賦值給了b
,第 8 行將b
陣列的第一個元素修改為 Singapore,這不會影響a
。所以會輸出,
a is [USA China India Germany France]
b is [Singapore China India Germany France]
複製程式碼
類似地,當陣列作為引數傳遞給函式時,它們按值傳遞,原陣列也不會變。
package main
import "fmt"
func changeLocal(num [5]int) {
num[0] = 55
fmt.Println("inside function ", num)
}
func main() {
num := [...]int{5, 6, 7, 8, 8}
fmt.Println("before passing to function ", num)
changeLocal(num) //num is passed by value
fmt.Println("after passing to function ", num)
}
複製程式碼
上述程式的第 13 行,我們將陣列num
當作引數傳遞給了函式changeLocal
。函式呼叫後將不會改變原num
陣列的值,程式輸出,
before passing to function [5 6 7 8 8]
inside function [55 6 7 8 8]
after passing to function [5 6 7 8 8]
複製程式碼
陣列的長度
陣列的長度通過把陣列當引數傳遞給len
函式來計算。
package main
import "fmt"
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
fmt.Println("length of a is",len(a))
}
複製程式碼
上述程式碼將輸出,length of a is 4
使用 range 迭代陣列
可以用for
迴圈來迭代一個陣列的所有元素。
package main
import "fmt"
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
for i := 0; i < len(a); i++ { //looping from 0 to the length of the array
fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}
}
複製程式碼
Run in playground
上述程式碼使用了for
迴圈迭代了陣列的所有元素,該程式將輸出,
0 th element of a is 67.70
1 th element of a is 89.80
2 th element of a is 21.00
3 th element of a is 78.00
複製程式碼
Go 提供了一種更好,更簡潔的方法,通過使用for
迴圈的range
形式迭代陣列。 range
返回索引和該索引處的值。讓我們使用range
重寫上面的程式碼,並計算所有元素的和。
package main
import "fmt"
func main() {
a := [...]float64{67.7, 89.8, 21, 78}
sum := float64(0)
for i, v := range a {//range returns both the index and value
fmt.Printf("%d the element of a is %.2f\n", i, v)
sum += v
}
fmt.Println("\nsum of all elements of a",sum)
}
複製程式碼
上述程式碼的第 8 行,for i, v := range a
就是該迴圈的形式。它將返回索引和該索引處的值。我們列印值並計算陣列a
的所有元素的和。該程式的輸出是,
0 the element of a is 67.70
1 the element of a is 89.80
2 the element of a is 21.00
3 the element of a is 78.00
sum of all elements of a 256.5
複製程式碼
在該例子中如果你只想要值,你可以使用_
佔位符替代返回索引的位置。
for _, v := range a { //ignores index
}
複製程式碼
類似的,值也可以被忽略。
多維陣列
目前位置我們建立的都是一維陣列,當然也可以建立多維陣列。
package main
import (
"fmt"
)
func printarray(a [3][2]string) {
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
func main() {
a := [3][2]string{
{"lion", "tiger"},
{"cat", "dog"},
{"pigeon", "peacock"}, //this comma is necessary. The compiler will complain if you omit this comma
}
printarray(a)
var b [3][2]string
b[0][0] = "apple"
b[0][1] = "samsung"
b[1][0] = "microsoft"
b[1][1] = "google"
b[2][0] = "AT&T"
b[2][1] = "T-Mobile"
fmt.Printf("\n")
printarray(b)
}
複製程式碼
在上面的程式中的第 17 中,使用簡短語法宣告瞭二維字串陣列a
。第 20 行末尾的逗號是必要的,這是因為詞法分析器根據簡單的規則自動插入分號。如果您有興趣瞭解更多,可以閱讀https://golang.org/doc/effective_go.html#semicolons。
另一個二維陣列b
在第 23 行被宣告。通過每個索引逐個新增字串是初始化二維陣列的另一種方法。
第 7 行中的printarray
函式,使用兩個for range
迴圈來列印二維陣列的內容。上述程式的輸出是
lion tiger
cat dog
pigeon peacock
apple samsung
microsoft google
AT&T T-Mobile
複製程式碼
這就是陣列。儘管陣列似乎足夠靈活,但它具有固定長度的限制,無法增加陣列的長度,而切片可以自動擴充長度。事實上,在 Go 中,切片比陣列更常見。
切片
切片是方便,靈活且功能強大的基於陣列的裝飾器。切片本身不擁有任何資料。它們只是對現有陣列的引用。
建立切片
一個元素型別為T
的切片用[ ]T
表示。
package main
import (
"fmt"
)
func main() {
a := [5]int{76, 77, 78, 79, 80}
var b []int = a[1:4] //creates a slice from a[1] to a[3]
fmt.Println(b)
}
複製程式碼
語法a[start:end]
表示用陣列a
索引的start
到end - 1
來建立一個切片,所以上述程式第 9 行的a[1:4]
建立了一個表示陣列a
從索引 1 到 3 的切片(左閉右開區間)。因此,切片b
的值為[77 78 79]
讓我們看下建立切片的另一種方式,
package main
import (
"fmt"
)
func main() {
c := []int{6, 7, 8} //creates and array and returns a slice reference
fmt.Println(c)
}
複製程式碼
上述程式碼的第 9 行,c := []int{6, 7, 8}
建立了一個 3 個整數的切片 c`。
修改切片
切片不擁有自己的任何資料。它只是底層陣列的表示,對切片所做的任何修改都將反映在底層陣列中。
package main
import (
"fmt"
)
func main() {
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before",darr)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after",darr)
}
複製程式碼
在上面程式的第 9 行中,我們從陣列的索引 2, 3, 4 建立了dslice
切片。 for
迴圈將這些索引中的值遞增 1。當我們在for
迴圈之後列印陣列時,我們可以看到對切片的更改影響到了陣列。該程式的輸出是
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 78 67 69 59]
複製程式碼
當許多切片共享相同的底層陣列時,每個切片所做的更改將反映在陣列中。
package main
import (
"fmt"
)
func main() {
numa := [3]int{78, 79 ,80}
nums1 := numa[:] //creates a slice which contains all elements of the array
nums2 := numa[:]
fmt.Println("array before change 1",numa)
nums1[0] = 100
fmt.Println("array after modification to slice nums1", numa)
nums2[1] = 101
fmt.Println("array after modification to slice nums2", numa)
}
複製程式碼
在第 9 行,numa[:]
中缺少了起始值和結束值。start
和end
的預設值分別為0
和len(numa)
。切片nums1
和nums2
共享相同的陣列。該程式的輸出是
array before change 1 [78 79 80]
array after modification to slice nums1 [100 79 80]
array after modification to slice nums2 [100 101 80]
複製程式碼
從輸出中可以清楚地看出,當切片共享同一個陣列時,每個陣列所做的修改都會反映在陣列中。
切片的長度和容量
切片的長度是指切片中元素的數量。切片的容量是指從切片的起始索引值開始到陣列末尾的元素數。
寫個程式碼來加深理解,
package main
import (
"fmt"
)
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
}
複製程式碼
在上面的程式中,fruitslice
是用fruitarray
的索引值 1 和 2 建立的。因此,長度為 2。
fruitarray
的長度為 7。fruiteslice
從fruitarray
的索引 1 開始建立。因此,fruiteslice
的容量是從索引 1 開始的fruitarray
中的元素,即從orange
開始,該值為 6。因此,fruiteslice
的容量為 6。該程式輸出length of slice 2 capacity 6.
切片可以重新切片到他的最大容量。如果超過容量將會導致程式執行時丟擲錯誤。
package main
import (
"fmt"
)
func main() {
fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
fruitslice = fruitslice[:cap(fruitslice)] //re-slicing furitslice till its capacity
fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))
}
複製程式碼
在第 11 行,fruitslice
被重新切片到他的最大容量,程式將輸出,
length of slice 2 capacity 6
After re-slicing length is 6 and capacity is 6
複製程式碼
使用 make 函式建立切片
func make([]T, len, cap) []T
通過傳遞型別,長度,容量來建立一個切片。容量引數是可選的,預設值是長度。
make
函式建立一個陣列並返回一個切片引用。
package main
import (
"fmt"
)
func main() {
i := make([]int, 5, 5)
fmt.Println(i)
}
複製程式碼
Run in playground
使用make
函式建立的切片預設會被零值填充,上述程式將輸出[0 0 0 0 0]
為切片增加元素
我們知道陣列是固定長度的,並且它們的長度不能擴容。而切片是動態的,可以使用append
函式將新元素增加到切片。func append(s []T, x ...T) []T
是append
函式的定義。
函式中的x ...T
參數列示函式可以接受一個變長的引數x
,這種型別的函式稱為變參函式。
但有一個問題可能會困擾你。切片的底層依賴了陣列,而陣列本身是固定長度,那麼切片是怎麼實現動態長度的呢?實現原理是,當新元 素新增到切片時,會建立一個新陣列。現有陣列的元素將複製到此新陣列,並返回此新陣列的新切片引用。新切片的容量現在是舊切片的兩倍(譯者注:有一個擴容演算法,並不都是兩倍),下面的程式碼將會讓我更容易理解。
package main
import (
"fmt"
)
func main() {
cars := []string{"Ferrari", "Honda", "Ford"}
fmt.Println("cars:", cars, "has old length", len(cars), "and capacity", cap(cars)) //capacity of cars is 3
cars = append(cars, "Toyota")
fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars)) //capacity of cars is doubled to 6
}
複製程式碼
在上述程式中,cars
的容量最初為 3。我們在第 8 行中為cars
新增了一個新元素,並把append(cars, "Toyota")
返回的切片賦值給cars
。現在cars
的容量增加了一倍,變成了 6。上述程式的輸出是
cars: [Ferrari Honda Ford] has old length 3 and capacity 3
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6
複製程式碼
切片的零值是nil
,一個nil
切片的長度和容量都為 0。能使用append
函式給nil
切片新增元素。
package main
import (
"fmt"
)
func main() {
var names []string //zero value of a slice is nil
if names == nil {
fmt.Println("slice is nil going to append")
names = append(names, "John", "Sebastian", "Vinay")
fmt.Println("names contents:",names)
}
}
複製程式碼
names
是一個nil
切片,我們給names
新增了 3 個字串,該程式輸出,
slice is nil going to append
names contents: [John Sebastian Vinay]
複製程式碼
也可以使用...
操作把一個切片新增到另一個切片,可以在變參函式這一章節學到更多關於該操作的內容。
package main
import (
"fmt"
)
func main() {
veggies := []string{"potatoes","tomatoes","brinjal"}
fruits := []string{"oranges","apples"}
food := append(veggies, fruits...)
fmt.Println("food:",food)
}
複製程式碼
在上述程式碼的第 10 行,food
切片使用veggies
切片新增fruits
切片建立。程式的輸出為,food: [potatoes tomatoes brinjal oranges apples]
切片作為函式的引數
切片的內部結構可以被認為是結構型別。這就是像這樣,
type slice struct {
Length int
Capacity int
ZerothElement *byte
}
複製程式碼
切片包含長度,容量和指向陣列的第 0 個元素的指標。當切片傳遞給函式時,即使它是按值傳遞的,指標變數也會引用相同的底層陣列。因此,當切片作為引數傳遞給函式時,函式內部所做的更改也會在函式外部顯示。讓我們寫一個程式來驗證一下。
package main
import (
"fmt"
)
func subtactOne(numbers []int) {
for i := range numbers {
numbers[i] -= 2
}
}
func main() {
nos := []int{8, 7, 6}
fmt.Println("slice before function call", nos)
subtactOne(nos) //function modifies the slice
fmt.Println("slice after function call", nos) //modifications are visible outside
}
複製程式碼
上述程式的第 16 行中的函式呼叫將每個切片元素遞減 2。當在函式呼叫之後列印切片時,這些修改是可見的。回想一下,這和陣列不同,在陣列中,對函式內部的陣列所做的修改在函式外部是不可見的。上述程式的輸出是,
slice before function call [8 7 6]
slice after function call [6 5 4]
複製程式碼
多維切片
跟陣列一樣,切片也支援多維
package main
import (
"fmt"
)
func main() {
pls := [][]string {
{"C", "C++"},
{"JavaScript"},
{"Go", "Rust"},
}
for _, v1 := range pls {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
複製程式碼
程式輸出,
C C++
JavaScript
Go Rust
複製程式碼
記憶體優化
切片引用了底層陣列,只要切片在記憶體中,就不能對陣列進行垃圾回收。在記憶體管理,這可能會引起關注。我們假設我們有一個非常大的陣列,我們有興趣只處理它的一小部分。此後,我們從該陣列建立一個切片並開始處理切片。需要注意的一點是,由於切片引用了該陣列,因此陣列仍將在記憶體中。
解決此問題的一種方法是使用copy
函式func copy(dst, src []T) int
來獲取該切片的副本。這樣我們就可以使用新的切片,原始陣列就可以被垃圾收集了。
package main
import (
"fmt"
)
func countries() []string {
countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
neededCountries := countries[:len(countries)-2]
countriesCpy := make([]string, len(neededCountries))
copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
return countriesCpy
}
func main() {
countriesNeeded := countries()
fmt.Println(countriesNeeded)
}
複製程式碼
在上述程式碼的第 9 行,neededCountries := countries[:len(countries)-2]
建立了一個countries
的切片。在第 11 行將neededCountries
複製給了countriesCpy
,隨後函式返回。現在,countries
的底層陣列將會被垃圾回收,因為neededCountries
不再被引用。