数组是一种长度不可改变的集合,go数组不仅如此,还是一个值变量,也就是在赋值和函数调用采用值传递,会产生数组的全量拷贝,需要耗费内存和时间,基于上述原因,多数情况下会优先选择使用slice。关于slice使用在细节上还有许多需要仔细研究的地方。
make
make 是slice 重要创建方式。
make([]T, length, capacity)
可以简写为
slice1 := make([]T, length)
length 表示slice中已经使用的数据长度
capacity 表示slice中指针执行的数组容量,如果缺省capacity 和length大小相等。
上代码:
package main
import "fmt"
func main() {
s := make([]int, 0)
fmt.Printf("s =%v, len= %d, cap = %d\n", s, len(s), cap(s))
}
运行输出:
s =[], len= 0, cap = 0
从运行结果可以看出,slice是数组的包装,它含:
指向数组的指针
slice 长度(slice当前数据个数)
slice容量(指向数组长度)
组合到一起成为slice,构成对数组引用。slice 具有数组所有特性,可以索引,遍历。
make函数用于初始化slice。
make([]T, length, capacity)
程序中,我们length 传值为0, capacity 缺省(缺省时cap = length),所以length= 0, capacity 也为零,slice 指向数组的指针为nil。我们一起看一下程序输出:
s =[], len= 0, cap = 0 就是length 传0值的结果。实际构造一个空slice。
capacity 传值大于零(注意:值必须大于等于 length),make 会分配一个长度为capcity 的数组。slice 指针等于数组首元素地址。数组中元素初始化为类型T的零值。
append
我们研究一下,append 到哪里?
上程序
package main
import "fmt"
func main() {
s := make([]int, 0, 10)
fmt.Printf("s=%v, len=%d, cap=%d\n", s, len(s), cap(s))
s1 := append(s, 1)
fmt.Printf("s1=%v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
}
s1 := append(s, 1) 为甚要这样写呢? 目的是让大家注意apend 函数将数据apend到哪里?我们看一下运行结果
s=[], len=0, cap=10
s1=[1], len=1, cap=10
从运行结果看:
apend 函数生成新的slice,不改变原来的slice。也就是apend 增加到新的slice。这样子不是很容易说清楚,我们例子进一步说明问题:
package main
import "fmt"
func main() {
a := [...]int{1, 2, 3, 4, 5, 5}
s := a[2:4]
s1 := append(s, 9)
fmt.Printf("s=%v, len=%d cap =%d\n", s, len(s), cap(s))
fmt.Printf("s1=%v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
fmt.Printf("a=%v, len=%d\n", a, len(a))
}
s=[3 4], len=2 cap =4
s1=[3 4 9], len=3, cap=4
a=[1 2 3 4 9 5], len=6
本例子增加一个数组,主要为了让大家看到append 到哪里了,实际上是append 到切片s指向的底层数组a。千语万语抵不上一张图更易于理解。

如图1所示,s := a[2:4] ,创建一个slice,该slice *data=&a[2], len = 2, cap =len(a) -2,构造后,s 数组指针指向数组a[2]的地址。

如图2所示,执行s1 := append(s, 9),
判断append之后元素个数没有超过slice容量,本例就是没有超过4,就是 不需要扩容。
新建的slice仍然指向数组a,本例指向a[2]的地址,如图中黄色箭头。append之后将slice索引2的位置更改为9,也就是图中青色位置。
程序输出,大家看到a[5]的位置变成9, 这个是两个slice(s和s2)索引2的位置,也就是第三个元素位置。
分析完append 到哪里。我们继续看append的实现。
append实现分析
append 实现3个功能:
构造新的slice
扩容
“增加”元素
1、2 、3都好理解,应该是slice 必备功能。唯一的疑问是“增加”为何要加引号。
表象上slice确实增加了元素,因为length 增加了。
因为本质append 是更改slice指向数组的对应索引位置的值。也就是单纯的数组索引访问。包装成append后避免了数组访问越界。
apend 实现分析完,剩下的问题是数组共享。虽然大部分不会影响我们的程序,但有时共享确实是错误的根源,在并发程序中尤其突出。共享很多时候会节约内存,有时情况却相反,比如一个大数组,做了一个小切片使用,如果仅有这个小切片引用数组,导致这个大数组不能释放。那么反而浪费内存。这些时候我们如何不共享呢。
这个时候最简单的办法是切片时指定容量为切片长度
我们是因为在原有数组切片或者在切片上切片引起的共享,如果切片时引入第三个参数, s:= a[1:2:1], 指定切片后容量,如果我们指定切片容量和我们切片长度一样。在append到s 的时候,会引起扩容,分配新数组给slice s。这时调用append函数就append 到新数组。
package main
import "fmt"
func main() {
a := []int{2, 2}
b := a[0:1:1]
b = append(b, 3)
fmt.Println(b, a)
b1 := a[0:1]
b1 = append(b1, 3)
fmt.Println(b1, a)
}
运行结果
[2 3] [2 2]
[2 3] [2 3]
结果第一行对应 6、7、8、9 行代码, append 后没有更改原来的slice ,说明没有共享。
结果第二行 对应11、12 行代码,append后更改原来的slice,就是共享引起的问题。希望写程序引起注意。