本节重点:
- 学会使用切片
切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型。
实际开发中我们很少使用数组,取而代之的是切片。切片是一个 长度可变的数组
创建切片
具有 T 类型元素的切片表示为[]T
package main
import (
"fmt"
)
func main() {
a := [5]int{76, 77, 78, 79, 80}
var b []int = a[1:4] //创建一个切片 a[1] to a[3]
fmt.Println(b)
}
使用语法 a[start:end]
创建一个从 a 数组索引 start
开始到 end - 1
结束的切片。因此,在上述程序的第 9 行中, a[1:4]
为从索引 1
到 3
创建了 a
数组的一个切片表示。因此, 切片 b
的值为 [77 78 79]
。
让我们看看另一种创建切片的方法。
package main
import (
"fmt"
)
func main() {
c := []int{6, 7, 8}
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 循环将这些索引中的值逐个递增。当重新使用 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() {
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))
}
在上面的程序中,从 fruitarray
的索引 1
和 2
创建fruitslice
。 因此,fruitlice
的长度为 2
。
fruitarray
的长度是 7。fruiteslice
是从 fruitarray
的索引 1 开始创建的。因此, fruitslice
的容量是从 fruitarray
索引为 1开始,也就是说从 orange
开始,该值为 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)]
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]
。
追加切片元素
数组的长度是固定的,它的长度不能增加。切片是动态的,使用 append
可以将新元素追加到切片上。append
函数的定义是 func append(s[]T,x ... T)[]T
。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))
cars = append(cars, "Toyota")
fmt.Println("cars:", cars, "has new length", len(cars), "and capacity", cap(cars))
}
在上面程序中,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
切片的函数传递
可以认为切片在内部由结构类型表示。看起来是这样的:
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)
fmt.Println("slice after function call", nos)
}
上面程序的行号 17 中,调用函数将切片中的每个元素递减 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++"},
{"Java"},
{"Go", "Rust"},
}
for _, v1 := range pls {
for _, v2 := range v1 {
fmt.Printf("%s ", v2)
}
fmt.Printf("\n")
}
}
程序的输出如下:
C C++
Java
Go Rust
注意:Go 使用 2x 算法来增加数组长度