Slice使用的正确姿势

slice是Go中的一个常用的数据结构,日常使用时类似Java中的List。但是这里包含的语言特性又与Java有很大区别。下面介绍一下我使用过程中感觉需要注意的一些问题。

slice的cap与len

在slice创建的时候,我们会指定两个参数len和cap

1
s := make([]int, 0, 10)

其中的len是指当前数组容纳的数据数量,cap是指整个数组分配的空间。
看一个例子来理解一下len的使用

1
2
3
4
5
6
7
8
9
s := make([]int, 5, 10)

for i := 1; i < 5; i++ {
s = append(s, i)
}

fmt.Println(s)

console: [0, 0, 0, 0, 0, 1, 2, 3, 4]

这里我们append了四次,但是却输出了九个数,为什么呢?因为我们在初始化的时候,通过默认值将s的前五个数值填充了,后续的append过程中就是在原来的基础上继续添加。

理解完了len,我们再来看看cap。从字面意思看,就是slice容量的意思,这个值可以在创建slice的时候不显示的赋予,这个时候他会和len保持一致。那么如果在给定了cap大小的slice上继续添加数据会发生什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type StA struct{
info string
}

s := make([]StA, 0, 1)

data1 := StA{"data1"}

s = append(s, data1)

s = append(s, StA{"info...."})

data1.info = "data2"

fmt.Println(s[0].info)

console: data1

这里按照以前的编程经验,应该会返回data2,但是结果返回的是data1。原因是当slice所需的长度,超过了他自身的容量时,slice会自动进行扩容,产生新的slice,然后将原slice中的数据copy到新的slice中。所以扩容后的s中存储的是data1的副本,修改data1已经对s产生不了任何影响了。

这里需要注意一点,虽然slice进行了copy,创建了新的slice2,然后将slice2 赋值给原来的slice。但是slice的指针地址并没有变。

1
2
3
4
5
6
7
8
9
10
11
12
   s0 := make([]int, 0)
s1 := append(s0, 1)
fmt.Println(unsafe.Pointer(&s0))

s0 = s1
fmt.Println(unsafe.Pointer(&s1))
fmt.Println(unsafe.Pointer(&s0))

console:
0xc42000a2c0
0xc42000a2e0
0xc42000a2c0

这是因为go中的赋值,发生了数据拷贝,而不是简单的指针指向的修改。关于这个问题,我们后续会单独进行一次分析。