有多种方式可以声明切片,那么不同的声明之间有什么需要注意的呢?
nil切片与长度为0的切片
nil切片:
var data []int
长度为0的切片:
var x = []int{}
它们两个有细微的差别,长度为0的切片在与nil
比较时会返回false
。在切片与JSON的转换中会用长度为0的切片来表示空切片。
声明切片时赋默认值
data := []int{1, 2, 3, 4}
如果在声明时知道切片所需的大小,但不知道默认值给什么时,最好用make
。根据所声明切片的长度和容量,可分为三种情况:
- 如果切片被用作缓冲区(buffer),那切片的长度应为非零值。
- 如果你明确知道你所需的切片大小,则切片长度和容量可设为一样的。
- 其他情况下,用
make
创建一个长度为0,容量为指定值的切片即可。要添加元素时用append
就好。
切片表达式
切片表达式可以在现有的切片的基础上创建子切片。它和python中的有点类似,不过不支持负数索引。语言描述不太直观,用代码演示更容易理解:
a := []int{1, 2, 3, 4}
// 不写起始偏移量则默认从0开始
b := a[:2]
// 不写结束偏移量则默认为切片的长度
c := a[1:]
// 指定起始和结束偏移量,左闭右开
d := a[1:3]
// 都不指定,则起始偏移量为0,结束偏移量为切片长度
e := a[:]
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
fmt.Println("d:", d)
fmt.Println("e:", e)
输出:
a: [1 2 3 4]
b: [1 2]
c: [2 3 4]
d: [2 3]
e: [1 2 3 4]
共享存储的切片
通过切片表达式得到的子切片是与原切片共享存储的,改变其中的元素会使原切片和子切片中的数据一起变化。
a := []int{1, 2, 3, 4}
b := a[:2]
c := a[1:]
a[1] = 20
b[0] = 10
c[1] = 30
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
输出:
a: [10 20 30 4]
b: [10 20]
c: [20 30 4]
如果使用append向新切片追加元素的话,同样也会对原切片产生影响:
a := []int{1, 2, 3, 4}
b := a[:2]
c := a[1:]
// 新切片的容量为老切片容量减去新切片起始索引
fmt.Println("cap before:", cap(a), cap(b), cap(c))
// b的长度为2容量为4,与a、c共享同一存储,
// 追加元素后,原来的数据被覆盖
// a[2]=30,c[1]=30
b = append(b, 30)
// c的长度为3,容量为3,追加元素后,发生扩容,
// 与a、b不再共享存储
c = append(c, 50)
// a的长度为4,容量为4,追加元素后,发生扩容,
// 与b不再共享存储
a = append(a, 60)
// 此时再改变a、b切片中元素的值已经不会相互影响了
a[0] = 80
b[1] = 90
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
fmt.Println("cap after:", cap(a), cap(b), cap(c))
输出:
cap before: 4 4 3
a: [80 2 30 4 60]
b: [1 90 30]
c: [2 30 4 50]
cap after: 8 4 6
因此,为了减少bug,在使用切片表达式创建的子切片时,最好不用append
。
如果实在需要用append
,那最好用全切片表达式来创建子切片。
全切片表达式在原来两个偏移量的基础上增加了一个表示子切片容量偏移量的参数。有点绕,看代码就清楚了:
a := []int{1, 2, 3, 4}
b := a[:2:2]
c := a[2:3:3]
a[1] = 20
fmt.Println("cap before:", cap(a), cap(b), cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
a = append(a, 5)
b = append(b, 6)
c = append(c, 7)
fmt.Println("cap after:", cap(a), cap(b), cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
输出:
cap before: 4 2 1
a: [1 20 3 4]
b: [1 20]
c: [3]
cap after: 8 4 2
a: [1 20 3 4 5]
b: [1 20 6]
c: [3 7]
可以看到,通过第三个参数限制了子切片与原切片共享容量的结束位置,这样在对b
使用append
时,因为b
的容量已经和其长度一致了,append
会申请新的内存,避免了对原数据的意外覆盖。