Golang教程:(十一)陣列和切片
這是本Golang系列教程的第十一篇。
陣列
陣列是型別相同的元素的集合。例如,整數 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)
}
var a [3]int
聲明瞭一個長度為 3 的整型陣列。陣列中的所有元素都被自動賦值為元素型別的 0 值。比如這裡 a
是一個整型陣列,因此 a
中的所有元素都被賦值為 0(即整型的 0 值)。執行上面的程式,輸出為:[0 0 0]
。
陣列的索引從 0
開始到 length - 1
結束。下面讓我們給上面的陣列賦一些值。
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)
}
a[0]
表示陣列中的第一個元素。程式的輸出為:[12 78 50]
。
(譯者注:可以用下標運算子([]
)來訪問陣列中的元素,下標從 0 開始,例如 a[0]
表示陣列 a
的第一個元素,a[1]
表示陣列 a
的第二元素,以此類推。)
可以利用速記宣告(shorthand declaration)的方式來建立同樣的陣列:
package main
import (
"fmt"
)
func main() {
a := [3]int{12, 78, 50} // shorthand 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。剩下的兩個元素被自動賦值為 0。程式 的輸出為:[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
是兩個不同型別的陣列。正是因為如此,一個數組不能動態改變長度。不要擔心這個限制,因為切片(slices
)可以彌補這個不足。
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])
}
}
上面的程式使用 for
迴圈遍歷陣列中的元素(索引從 0
到 len(a) - 1
)。上面的程式輸出如下:
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 提供了一個更簡單,更簡潔的遍歷陣列的方法:使用 range for。range 返回陣列的索引和索引對應的值。讓我們用 range for 重寫上面的程式(除此之外我們還計算了陣列元素的總和)。
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
是 range 形式的 for 迴圈。range 將返回陣列的索引和相對應的元素。我們列印這些值並計算陣列 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 。
在第 23 行聲明瞭另一個二維陣列 b
,並通過索引的方式給陣列 b
中的每一個元素賦值。這是初始化二維陣列的另一種方式。
第 7 行宣告的函式 printarray
通過兩個巢狀的 range for 列印二維陣列的內容。上面程式的輸出為:
lion tiger
cat dog
pigeon peacock
apple samsung
microsoft google
AT&T T-Mobile
以上就是對陣列的介紹。儘管陣列看起來足夠靈活,但是陣列的長度是固定的,沒辦法動態增加陣列的長度。而切片卻沒有這個限制,實際上在 Go 中,切片比陣列更為常見。
切片
切片(slice)是建立在陣列之上的更方便,更靈活,更強大的資料結構。切片並不儲存任何元素而只是對現有陣列的引用。
建立切片
元素型別為 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]
到 a[end -1]
的切片。在上面的程式中,第 9 行 a[1:4]
建立了一個從 a[1]
到 a[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 的 int 陣列,並返回一個切片給 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 行,我們建立了一個從 darr[2]
到 darr[5]
的切片 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[:]
中缺少了開始和結束的索引值,這種情況下開始和結束的索引值預設為 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]
從輸出結果可以看出,當多個切片共享同一個陣列時,對每一個切片的修改都將會反映到這個陣列中。
切片的長度和容量
切片的長度是指切片中元素的個數。切片的容量是指從切片的起始元素開始到其底層陣列中的最後一個元素的個數。
(譯者注:使用內建函式 cap
返回切片的容量。)
讓我們寫一些程式碼來更好地理解這一點。
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
}
在上面的程式中,建立了一個以 fruitarray
為底層陣列,索引從 1
到 3
的切片 fruitslice
。因此 fruitslice
長度為 2
。
fruitarray
的長度是 7。fruiteslice
是從 fruitarray
的索引 1
開始的。因此 fruiteslice
的容量是從 fruitarray
的第 1
個元素開始算起的陣列中的元素個數,這個值是 6
。因此 fruitslice
的容量是 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)
}
用 make
建立的切片的元素值預設為 0 值。上面的程式輸出為:[0 0 0 0 0]
。
追加元素到切片
我們已經知道陣列是固定長度的,它們的長度不能動態增加。而切片是動態的,可以使用內建函式 append
新增元素到切片。append
的函式原型為:append(s []T, x ...T) []T
。
x …T 表示 append
函式可以接受的引數個數是可變的。這種函式叫做變參函式。
你可能會問一個問題:如果切片是建立在陣列之上的,而陣列本身不能改變長度,那麼切片是如何動態改變長度的呢?實際發生的情況是,當新元素通過呼叫 append
函式追加到切片末尾時,如果超出了容量,append
內部會建立一個新的陣列。並將原有陣列的元素被拷貝給這個新的陣列,最後返回建立在這個新陣列上的切片。這個新切片的容量是舊切片的二倍(譯者注:當超出切片的容量時,append
將會在其內部建立新的陣列,該陣列的大小是原切片容量的 2 倍。最後 append
返回這個陣列的全切片,即從 0 到 length - 1 的切片)。下面的程式使事情變得明朗:
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。在第 10 行我們追加了一個新的元素給 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
切片的 0 值為 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
,並且我們把 3 個字串追加給 names
。程式的輸出為:
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行將 fruits
追加到 veggies
並賦值給 food
。...
操作符用來展開切片。程式的輸出為:food: [potatoes tomatoes brinjal oranges apples]
。
切片作為函式引數
可以認為切片在內部表示為如下的結構體:
type slice struct {
Length int
Capacity int
ZerothElement *byte
}
可以看到切片包含長度、容量、以及一個指向首元素的指標。當將一個切片作為引數傳遞給一個函式時,雖然是值傳遞,但是指標始終指向同一個陣列。因此將切片作為引數傳給函式時,函式對該切片的修改在函式外部也可以看到。讓我們寫一個程式來驗證這一點。
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
}
在上面的程式中,第 17 行將切片中的每個元素的值減2
。在函式呼叫之後列印切片的的內容,發現切片內容發生了改變。你可以回想一下,這不同於一個數組,對函式內部的陣列所做的更改在函式外不可見。上面的程式輸出如下:
array before function call [8 7 6]
array 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
並在下一行返回 countriesCpy
。現在陣列 countries
可以被垃圾回收,因為 neededCountries
不再被引用。
我(原文作者)已經將我們討論的所有概念彙總到一個程式中,你可以從 github 下載。
陣列和切片的介紹到此結束。感謝閱讀。