slice概念
slice表示一个拥有相同类型元素的可变长度的序列。slice通常写成[]T,其中元素的类型都是T,这点儿类似于java中的泛型,可以接受未知类型的变量。
slice是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素。slice的底层是数组,slice有三个属性:指针、长度、容量;
指针:是指向每个元素的地址;
长度:slice所存储的元素个数;
容量:slice的大小;
slice细节
Go内置的
len()
和cap()
函数可以返回slice的长度和容量大小。一个底层数组可以对应多个slice,这些slice可以引用数组的任何位置,彼此之间元素也可以重叠。值得注意的是,如果slice的引用超过了被引用对象的容量,即cap(s),就会导致程序宕机;如果引用超出了被引用对象的长度,即len(s),那么最终slice会比原来的slice长。
注意求字符串(string)子串操作和对字节slice([]byte)做slice操作是相似的,它们都写作x[m:n],都返回原始字节的一个子序列,引用方式也是相同的,两个操作都是消耗常量的时间。
和数组不同,slice无法直接做比较,因此不能用==来比较两个slice是否相同。标准库里面提供了高度优化的函数
bytes.Equal
来比较两个字节slice。但是对于其他类型的slice就需要我们自己写函数来比较。
func equal(x,y []string) bool{
if len(x) != len(y){
return false
}
for i := range x {
if x[i] != y[i]{
return false
}
}
return true
}
- 你或许会奇怪为什么slice不可以直接使用
==
操作符做比较。这里有两个原因,
第一,数组的元素是直接的,而slice的元素是非直接的,有可能slice可以包含它自身。我们虽然有办法处理这种情况,但是没有一种简单、高效、直观的方法。
第二,slice的元素不是直接的,如果底层数组元素改变,同一个slice在不同的时间就会拥有不同的元素。由于散列表仅对元素做浅拷贝,这就要求散列表的键在整个生命周期都保持不变。因为slice需要深度比较,所以就不能用slice作为map的键。对于引用类型的指针和通道,操作符==
检查的是引用相等性,即它们是否指向相同的元素。如果有一个相似的slice相等性比较功能,或许会比较有用,也能解决slice作为map键的问题。由于slice涉及的情况比较多,因此最安全的方法就是不要直接比较slice。
- slice唯一允许的就是和nil进行比较:
summmer := []int
if summer == nil{/**...*/}
slice类型的零值是nil,值为nil的slice没有对应的底层数组,且长度和容量都是0。但是也有非nil的slice长度和容量是0,例如[]int{}或make([]int,3)[3:]
对于任何类型,如果它们的值是nil,那么则可以这样表示:
var s []int
s = nil
s = []int(nil)
s = []int{}
- 如果需要检查一个slice是否为空,那么可以使用
len(s) == 0
,不能使用s == nil
,因为存在s != nil
,slice也可能是空的情况。 - 内置函数make可以创建一个具有指定元素类型、长度、容量的slice。其中容量参数可以省略,在这种情况下,slice的长度和容量相等。
make([]T,len)
make([]T,len,cap) //等效于make([]T,cap)[:len]
上面的代码中,make创建了一个无名数组并返回了它的一个slice;这个数组仅可以通过slice来访问。上面的第一行代码中,所返回的slice引用了整个数组。第二行代码中,slice只引用了数组的前len个元素。
append函数
Go语言的内置函数append()
可以用来将元素追加到slice后面。
var runes []rune
for _,r := range "hello world"{
runes = append(runes,r)
}
fmt.Printf("%q\n,runes")//"['H' 'e' 'l' 'l' 'o' 'w' 'o' 'r' 'l' 'd']"
append()
函数对于理解slice的工作原理很重要,我们实现一个appendInt()
函数,再来深刻理解一下:
func appendInt(x []int, y int) []int{
var z []int
zlen := len(x)+1
if zlen <= cap(x){
//slice仍有增长空间,扩展slice内容
z = x[:zlen]
}else{
//slice已无空间,为它分配一个新的的底层数组
//为了达到分摊的线性复杂,容量扩展一倍
zcap := zlen
if zcap < 2*len(x){
zcap = 2 * len(x)
}
z = make([]int,zlen,cap)
copy(z,x)//内置的copy函数
}
z[len(x)] = y
return z
}
每一次调用appendInt
都必须检查slice是否仍有足够的容量来存储数组中的新元素。如果slice容量足够,那么它就会定义一个新的slice(仍然引用原始底层数组),然后将新元素与复制到新的位置,并返回这个新的slice。输入参数slice x和函数返回值slice z 拥有相同的底层数组。
如果slice容量不够容纳增长的元素,appendInt
函数必须创建一个拥有足够容量的新的底层数组来存储新元素,然后将元素从slice x复制到这个数组,再将新元素y追加到数组后面,返回值slice z 将和输入参数slice x引用不同的底层数组。
copy函数用来为两个拥有相同类型的元素slice复制元素,copy函数的第一个参数时目标slice,第二个参数是源slice。
出于效率的考虑,新创建的数组容量会比实际容纳slice x 和slice y所需要的最小长度更大一点。在每次数组容量扩展时,通过扩展一倍的容量来减少内存分配的次数,这样也可以保证追加一个元素所消耗的时间是固定的。
func main(){
var x,y []int
for i := 0; i < 10; i++{
y = appendInt(x,i)
fmt.Printf("%d cap=%d\t%v\n",i,cap(y),y)
x = y
}
}
slice就地修改
slice就地修改在实际开发中能够很方便的帮助我们简化代码,减少出错的机会。
package main
import "fmt"
//nonmpty返回一个新的slice,slice中的元素都是非空字符串
//在函数调用过程中,底层数组的元素发生了改变
func nonempty(strings []string) []string{
i := 0
for _,s := range strings{
if s != ""{
string[i] = s
i++
}
}
return string[:i]
}
仔细阅读上述代码,我们就会发现输入的[]string 和返回的[]string是拥有相同的底层数组。