切片(slice)本身并非动态数组或数组指针。它内部通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域。
切片(slice)是建立在数组之上的更方便,更灵活,更强大的数据结构。切片并不存储任何元素而只是对现有数组的引用。
创建切片
元素类型为 T 的切片表示为: [ ]T。
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]
的切片。在上面的案例中, a[1:4]
创建了一个从 a[1]
到 a[3]
的切片。因此 b
的值为:[77 78 79]
。
另外一个创建方式
func main() {
c := []int{6, 7, 8} //creates and array and returns a slice reference
fmt.Println(c)
}
在上面的案例中,c := []int{6, 7, 8}
创建了一个长度为 3 的 int 数组,并返回一个切片给 c。
修改切片
切片本身不包含任何数据。它仅仅是底层数组的一个上层表示。对切片进行的任何修改都将反映在底层数组中。
func main() {
arr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
slice := arr[2:5]
fmt.Println("array before",arr)
for i := range slice {
slice[i]++
}
fmt.Println("array after",arr)
}
我们创建了一个从 arr[2]
到 arr[5]
的切片 slice
。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]
当若干个切片共享同一个底层数组时,对每一个切片的修改都会反映在底层数组中。
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)
}
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 返回切片的容量
)
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
到 3
的切片 fruitslice
。因此 fruitslice
长度为2
。
fruitarray
的长度是 7。fruiteslice
是从 fruitarray
的索引 1
开始的。因此 fruiteslice
的容量是从 fruitarray
的第 1
个元素开始算起的数组中的元素个数,这个值是 6
。因此 fruitslice
的容量是 6
。输出为:length of slice 2 capacity 6
。
切片的长度可以动态的改变(最大为其容量)。任何超出最大容量的操作都会发生运行时错误。
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))
}
在上面的案例中, 修改 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 函数将会创建一个数组并返回它的切片。
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 的切片
)。下面的程序使事情变得明朗:
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。我们追加了一个新的元素给 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 切片追加值。
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]
可以使用 ...
操作符将一个切片追加到另一个切片末尾:
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
}
可以看到切片包含长度、容量、以及一个指向首元素的指针。当将一个切片作为参数传递给一个函数时,虽然是值传递,但是指针始终指向同一个数组。因此将切片作为参数传给函数时,函数对该切片的修改在函数外部也可以看到。让我们写一个程序来验证这一点。
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 before function call [8 7 6]
slice after function call [6 5 4]
多维切片
同数组一样,切片也可以有多个维度。
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
来创建该切片的一个拷贝。这样我们就可以使用这个新的切片,原来的数组可以被垃圾回收。
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
并排除最后两个元素的切片。将 neededCountries
拷贝到 countriesCpy
并在下一行返回 countriesCpy
。现在数组countries
可以被垃圾回收,因为 neededCountries
不再被引用。