原文是将数组和切换放在同一节介绍,我个人觉得篇幅太长因此将切片(Slice)单独拆成一节。 切片是在数组的基础上包装出来的一个数据类型,当时更加方便、灵活。与数组不同,切片是引用类型(引用的仍是底层数组)。中文切片 翻译的可能有点生硬,但是也没其他更好的词,后面我会交替着用中文切片 和英文slice 。
声明切片
slice的声明方式为[]T
,跟数组的声明方式仅有一个区别,就是方括号内没有n
或...
。
Copy 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)
}
Go Playground在线运行
a[start:end]
用来基于数组a
创建一个切片,切取的元素范围为start
到end -1
。因此a[1:4]
切取了数组a
的索引为1
到3
的元素,然后赋值给切片元素b
。因此切片b
的元素为[77 78 79]
。这里我们声明切片b
也可以使用简写形式:b := a[1:4]
。
我们也可以通过字面量来声明并初始化切片:
Copy package main
import "fmt"
func main () {
c := [] int { 6 , 7 , 8 } //creates and array and returns a slice reference
fmt. Println (c)
}
Go Playground在线运行 执行结果如下:[6 7 8]
。
修改切片
切片仅仅是在数组的基础上包装出来的一个数据类型,修改切片元素时本质上修改的还是底层的数组。
Copy 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)
}
Go Playground在线运行 这里的切片dslice
是基于数组darr
创建的,然后更新了dslice
的值,打印结果如下:
Copy array before [ 57 89 90 82 100 78 67 69 59 ]
array after [ 57 89 91 83 101 78 67 69 59 ]
可以看出我们修改切片的值归根结底还是修改的底层数组的值。
基于同一个数组创建可以创建多个slice,其中通过任何一个slice改变元素,会影响到其他slice和底层的数组。
Copy 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" ,numa)
nums1[ 0 ] = 100
fmt. Println ( "array after modification to slice nums1" , numa, nums1, nums2)
nums2[ 1 ] = 101
fmt. Println ( "array after modification to slice nums2" , numa, nums1, nums2)
numa[ 0 ] = 200
fmt. Println ( "array after modification to array numa" , numa, nums1, nums2)
}
Go Playground在线运行 这里的numa[:]
省去了起止索引,这时会使用默认的起止索引值,0
和len(numa)
,也就是说会切取数组的所有元素。切片nums1
和nums2
共享一个底层数组numa
。执行结果如下:
Copy array before change [78 79 80]
array after modification to slice nums1 [100 79 80] [100 79 80] [100 79 80]
array after modification to slice nums2 [100 101 80] [100 101 80] [100 101 80]
array after modification to array numa [200 101 80] [200 101 80] [200 101 80]
可以看出,由于切片nums1
和nums2
底层共享数组numa
,因此这两个切片,任何一个改变底层的元素都会反应到另一个切片上面(本质上都是改变的数组的元素)。不过这种共享一个数组的特性最好不要用,因为一个切片在容量吃紧时会重新开辟新的底层数组,所以本来共享的好好的,说不定啥时候就翻脸不认人了,这点下面马上会介绍。
切片的长度和容量
切片长度 :切片包含的元素个数,通过len
函数获取。
切片容量 :底层数组从切片切取起始索引 开始到数组末尾 的长度,通过cap
函数获取。
我们写个例子来理解下这点。
Copy 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))
}
Go Playground在线运行 这里切片fruitslice
切取了数组中索引值为[1, 3)
的元素,因此切片长度为2。由于切片从索引1
开始切取,从该索引为止到数组末尾长度为6,因此切片容量为6。输出如下:
Copy length of slice 2 capacity 6
我们不仅可以在一个数组上切片,还可以再一个切片的基础上再次切片,但是切片切取上限时切片的当前容量。
Copy 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))
fmt. Println (fruitslice)
}
Go Playground在线运行 这里切片fruitslice
按最大容量重新切片。输出结果如下:
Copy length of slice 2 capacity 6
After re-slicing length is 6 and capacity is 6
[orange grape mango water melon pine apple chikoo]
追加元素
数组的长度固定不可变,因此不太灵活。而切片是动态长度可变的,可以通过append
函数不断的给切片添加元素。append
函数的定义如下:
Copy func append (s [] T , x ... T ) [] T
这里的x ...T
表示该函数接收可变数量的参数(这个主题我们下一节可变参数 会详细讨论)。
现在你估计会问,既然数组长度是固定不可变的,切片底层也是基于数组实现的,那么切片怎么做到动态长度可变的呢?
答案其实很简单,跟某些其他编程语言实现的机制类似,Golang中的切片会在底层数组长度吃紧时重新开辟新的、更大的数组,然后将原数组中的所有元素拷贝进来。这点可以通过一个例子来说明:
Copy 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
}
Go Playground在线运行 这里切片cars
初始容量是3,接着我们添加了一个新元素,然后该切片底层重新开辟了新数组,容量翻了一倍,达到了6。输出如下:
Copy cars: [Ferrari Honda Ford] has old length 3 and capacity 3
cars: [Ferrari Honda Ford Toyota] has new length 4 and capacity 6
现在我们再次看一下前面数组共享的例子:切片nums1
修改元素会反应到切片nums2
和底层的数组numa
上,这有一个前提就是必须是共享一个底层数组,如果切片nums1
由于不停的append
元素导致底层数组更换,就不再会跟nums2
共享底层数组numa
,也就不会再互相影响了:
Copy 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" ,numa)
nums1[ 0 ] = 100
fmt. Println ( "array after modification to slice nums1" , numa, nums1, nums2)
nums2[ 1 ] = 101
fmt. Println ( "array after modification to slice nums2" , numa, nums1, nums2)
numa[ 0 ] = 200
fmt. Println ( "array after modification to array numa" , numa, nums1, nums2)
nums1 = append (nums1, 36 )
nums3 := nums1[:]
nums1[ 0 ] = 300
fmt. Println ( "array after modification to slice nums1" , numa, nums1, nums2, nums3)
nums1 = append (nums1, 37 )
nums1[ 0 ] = 400
fmt. Println ( "array after modification to slice nums1" , numa, nums1, nums2, nums3)
nums1 = append (nums1, 38 , 39 , 40 , 50 , 60 , 70 , 80 , 90 , 100 , 101 , 102 )
nums1[ 0 ] = 500
fmt. Println ( "array after modification to slice nums1" , numa, nums1, nums2, nums3)
}
Go Playground在线运行
切片 类型的零值为nil
,称为空切片。空切片的长度和容量均为0。切片是动态的,即使是空切片也可以动态添加新元素:
Copy 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)
fmt. Println ( "names capacity:" , cap (names))
names = append (names, "Tom" )
fmt. Println ( "names capacity:" , cap (names))
names = append (names, "Joy" )
fmt. Println ( "names capacity:" , cap (names))
}
}
Go Playground在线运行 这里切片names
声明时未初始化,值为nil
。该程序执行结果如下:
Copy slice is nil going to append
names contents: [John Sebastian Vinay]
names capacity: 4
names capacity: 4
names capacity: 8
我们可以借助append
函数的可变参数(x ...T
),将一个切片所有元素追加到另外一个切片中:
Copy package main
import "fmt"
func main () {
veggies := [] string { "potatoes" , "tomatoes" , "brinjal" }
fruits := [] string { "oranges" , "apples" }
food := append (veggies, fruits ... )
fmt. Println ( "food:" ,food)
}
Go Playground在线运行 这里,我们将切片fruits
中的所有元素按顺序添加进切片food
中。执行结果如下:
Copy food: [potatoes tomatoes brinjal oranges apples]
使用make
声明切片
可以使用make
函数来声明切片,语法如下:
Copy func make ([] T , len , cap ) [] T
其中第三个参数cap
非必填,默认值为跟len
相同。make
函数原理就是创建一个数组,并基于该数组切出一个切片返回。
Copy package main
import "fmt"
func main () {
i := make ([] int , 5 , 5 )
fmt. Println (i)
}
Go Playground在线运行 使用make
创建的切片会自动填充零值,该程序执行结果为[0 0 0 0 0]
。
切片类型的函数参数
学过C语言 的应该都听过值传递 和引用传递 ,不了解的话可以参考这篇文章 。这里说的变量传递其实就是将一个变量赋值给另外一个变量,包括函数传参。
值传递是说变量赋值时直接传递变量的值,而不是变量的引用(或地址);而引用传递则是传递变量的引用,并不会传递变量本身的值。听起来比较抽象,其实非常简单,试想一下,假如某个数组变量存储了非常非常多的元素,每次传递该变量都将所有的元素考过来考过去效率肯定非常低,但是直接传递该数组变量的引用(或地址)效率就非常高。这里我们仅讨论下值传递和引用传递的不同,这二者并没有绝对的好坏,只不过不同的传递方式适用于不同的场景罢了,比如函数式编程就推崇函数调用时按值传递,函数应该是纯函数,每次以相同的参数去调用一个纯函数应该是幂等的。
Golang中的切片可以按以下结构理解:
Copy type slice struct {
Length int
Capacity int
ZerothElement *byte
}
可看出一个切片主要包括三项:长度,容量以及首个切片元素的指针。当将切片作为参数传递个函数进行调用时,虽然本质上仍然是按值传递,但是指针指向的底层数组还是同一个。因此,如果我们在函数内部修改了切片参数的元素,也会影响到原切片。
Copy 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
}
Go Playground在线运行 这里我们在函数subtactOne
内部将切片中的所有元素减2,这个修改也影响到了函数外的切片。执行结果如下:
Copy slice before function call [8 7 6]
slice after function call [6 5 4]
注意这一点跟传递数组参数不一样。
多维切片
跟数组类型,切片也支持多维。
Copy 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" )
}
}
Go Playground在线运行 执行结果如下:
Copy C C++
JavaScript
Go Rust
内存占用优化
前面已经介绍过了,切片本质上底层还是引用了一个数组。只要切片还在内存中未被回收,其底层的数组也不会被回收(垃圾回收机制)。如果底层的数组非常大可能会影响内存使用效率。例如,如果底层的数组非常大,但是我们关心和操作的仅仅是其中的一小部分,这时整个数组都处于被引用状态,无法被回收。
这种情况下我们可以通过copy
方法将切片拷贝成一个新的切片,进而达到释放旧切片及其底层数组的目的。copy
函数语法为func copy(dst, src []T) int
。
Copy 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)
}
Go Playground在线运行 这里我们将切片neededCountries
拷贝到countriesCpy
。后面neededCountries
就不会被继续引用,因此其占用的底层数组就会得以释放。