文章目录
导言
- 原文链接: Part 11: Arrays and Slices
- If translation is not allowed, please leave me in the comment area and I will delete it as soon as possible.
数组
数组是相同类型元素的集合。举个例子,[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
的整型数组。数组中的每个元素,都会被自动分配为该类型的零值。在这个例子中,a
是一个整型数组,所以它的所有元素将被分配为 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]
。
接下来,我们使用 快捷声明 创建一个相同的数组。
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)
}
在上面的程序,a := [3]int{12}
声明了一个长度为 3
的数组,但只为其提供了一个值 12
。剩余的 2
个元素将被自动分配为 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
是不同的类型。也因为这个原因,数组大小不能改变。不必担心,因为 切片 可以克服这个限制。
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
}
在上面这个程序,我们试图把 [3]int
数组 分配给[5]int
数组,但这是不允许的,编译器将会抛出一个异常:
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)
}
在上面程序中,a
的拷贝被分配给 b
。随后,数组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)
}
在上面程序中,数组 num
以值传递的方式传递给 函数changeLocal
。因此,原始数组并不会因函数调用而改变。这个程序输出如下:
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 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)
}
在上面的程序中,for i, v := range a
就是 for
循环的 range
形式。运行这个程序,输出如下:
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)
}
在上面的程序中,我们使用快捷句式声明了一个 二维数组a
。第 20
行的逗号是必须的,这是因为如果没有逗号,词法分析器会自动的插入分号,从而导致错误。如果你有兴趣,可以访问这个 网站 进行深入理解。
在上面的代码中,我们还声明了一个 二维数组b
,并一对一的对其赋值。这种方法也可以初始化二维数组。
printarray
函数使用了两个嵌套的 for
循环输出二维数组的内容。运行上面的程序,输出如下:
lion tiger
cat dog
pigeon peacock
apple samsung
microsoft google
AT&T T-Mobile
这就是数组。虽然数组看起来已经足够灵活,但数组长度是固定的,这是它的缺点。我们不能增长数组的长度。于是,切片 出现了。事实上,在 Go
语言 中,切片
比 数组
更常用。
切片
切片是数组之上的封装,它方便、灵活、功能强大。切片并不会拥有原始数据,它们只是引用了数组。
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
的基础上,创建了一个切片,索引区间为 [0, end - 1]
。所以,a[1:4]
创建了一个切片,这个切片引用了数组区间 [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)
}
在上面的程序中,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)
}
在上面的程序中,我们创建了一个 切片dslice
,引用了数组索引 2, 3, 4
。for
循环 使切片的元素都增加 1
。运行这个程序,我们将会发现: 对切片的修改作用到了底层数组。输出如下:
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)
}
在上面的程序中,num[:]
其实是 num[0:len(num)]
的简写形式。由于 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 fruitslice is 2 and capacity is 6
}
在上面的程序中,fruitslice
引用了 fruitarray
的 1、2
索引,因此 fruitslice
长度为 2
。
fruitarray
的长度是 7
。fruitslice
引用了 fruitarray
的 1、2
索引,因此它的容量为 6
— 因为它引用的开始索引是 1
,如果从 0
开始引用,那么它的容量就是 7
。
运行上面的程序,输出将是:
length of slice 2 capacity 6
对于一个切片,它能访问的最大索引为 容量 - 1
。 超过最大索引的访问将会引发运行时错误。
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))
}
运行上面的程序,输出如下:
length of slice 2 capacity 6
After re-slicing length is 6 and capacity is 6
使用 make
创建切片
为 func make([]T, len, cap) []T
函数 传递切片的类型、长度和容量,可以创建一个切片。cap
参数是可选的,默认等于 len
。make
函数将会创建一个数组,并返回一个引用该数组的切片。
package main
import (
"fmt"
)
func main() {
i := make([]int, 5, 5)
fmt.Println(i)
}
使用 make
创建切片时,切片元素的初始值是对应类型的零值。上面的程序输出为 [0 0 0 0 0]
。
为切片追加元素
我们已经知道了,数组的长度是固定的。但,切片是动态的,使用 append
函数 ,我们可以为其追加元素。append
函数 定义为 func append(s []T, x ...T) []T
。
在函数定义中,x ...T
表示 x
是一个接收可变长度的参数。相应的,拥有可变长度参数的函数,被称为 可变函数(variadic functions)
。
现在,你可能会疑惑: 切片的底层是一个数组,而数组长度是固定的,那么切片长度为何可变呢?解释是: 当我们为切片追加元素的时候,一个新数组将会被创建,随后,原数组的元素将被拷贝给新数组,并返回一个对新数组的引用。新切片的容量将是旧切片的两倍。
这里有点谬误,因为有时并不会创建一个新数组,只有当原数组装不下所有追加后的数据时,此时才会创建一个新数组去容纳。
新切片的容量也不一定总是旧切片的两倍,当旧切片的长度小于1024
时,新切片的容量才是旧切片的两倍
来看个代码。
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
。当我们为其分配一个新的元素 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
函数 为其追加元素。
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
个元素。程序输出如下:
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)
}
在上面的程序中,我们通过 append
函数,为 fruits
切片 追加一个切片 veggies
,并把返回的引用传递给 food
。运行上面的程序,输出如下:
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
}
这个函数的作用就是:将切片的每个值都增加 2
。在函数调用结束后,输出 slice
,我们会发现,这些改变影响了原切片。回忆一下,当我们在一个函数内部修改数组时,这些改变并不会作用于原数组。运行上面的程序,输出如下:
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
函数。copy
函数 定义为 func copy(dst, src []T) int
,它将生成一个原切片的深拷贝。于是,我们可以使用 copy
来产生新切片,从而使底层的数组能被垃圾收集。(可能有点迷惑,看下面)
假如原数组长度是
1000
,a
切片引用了它的3
个元素,这使得原数组的内存不能被回收。于是我们可以使用copy
函数,copy(b, a)
会在内存中生成一个长度为3
的底层数组,并把a
的数组元素传递给该底层数组,之后让b
指向这个底层数组。于是,当a
对原数组的引用被移除,长度为1000
的原数组就可以被垃圾回收,而我们还能使用原数组的数据。
来看段代码:
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)
}
在上面程序中,neededCountries := countries[:len(countries)-2]
在 countries
数组 之上创建了一个切片。copy(countriesCpy, neededCountries)
将 neededCountries
深拷贝给 countriesCpy
,随后返回 countriesCpy
。此时 countries
数组 就可以被垃圾回收,因为 neededCountries
不再引用它了。
原作者留言
我已经把上面的概念整合到了一个程序,你可以在 github 下载。
优质内容来之不易,您可以通过该 链接 为我捐赠。
最后
感谢原作者的优质内容。
这是我的第一次翻译,欢迎指出文中的任何错误。