Golang 中 Slice 的坑
深入分析使用 Go Slice 时遇到的一个坑
最近在学 Golang,发现 Slice 的一个坑。引发坑的原因是多个 Slice 可以共享底层的数据。
Golang 的 Slice 类似于 C++ 的 std::vector<T>,功能上是动态数组。
参考代码 golang 1.11
一、底层原理
slice 的结构体:
type slice struct { |
看一下动态数组的底层是如何扩容的。首先判断是否有剩余空间容纳新元素,没有的话就重新分配一个适合的空间(一般为原空间的 2 或 1.5 倍),然后把原先的元素移动到新的空间中。之后再把新元素移动到新的空间中。
这种增长策略可以减少开销,摊还分析得到 append 的时间复杂度为 O(1)。
我们以 { len: 0, cap: 0},增长因子为 2 举例。下图为依次添加 1,2,3,4,5 之后动态数组的变化。实线框为已有元素,虚线框为剩余空间。
当然 go 中的实现有多种增长策略,情况略微复杂。下面是 go 1.11源码中的增长策略。
// growslice in src/runtime/slice.go |
Slice 切片操作 s[i:j] 会创建新的 Slice,而且多个 Slice 底层共享同一内存空间,不是 C++ 那种直接把元素 copy 或 move 进去。比如:
s1 := []int{1,2,3,4,5} |
这样就会引发一些较坑的问题。下面以实际例子说明。
二、实际例子
func main() { |
可以看到最后 x 和 y 都为 [0 1 2 4],这样的结果就不符合本来的期待了。分析一下为什么出现这样的现象。
由于增长策略的问题,在 append(s, 2) 之后,底层数组为 [0 1 2],s.len == 3, s.cap == 4。
当 x := append(s, 3) 时,s 有足够的容量放置元素 3。由于 x 和 s 的底层数据指针相同,此时 s.len 和 s.cap 没有发生改变,x.len == 4, x.cap == 4,底层数组变为 [0 1 2 3]。
当 y := append(s, 4) 时,仍然检测到 s 有足够的容量放置元素 4,y.len == 4, y.cap == 4,此时 s, x, y 三者引用相同的底层数组,底层数组变为 [0 1 2 4],也就解释了为何出现了上面的现象。
再来看一个把 Slice 作为函数参数的问题。Slice 结构体为值传递。
func main() { |
实质上 nums, nums1, nums2 的底层数组指针值相同,不过 Slice 按值传递。
modifySlice 就相当于修改指针副本的值。就类似于
void modifyPointer(int* p) { |
modifyElement 修改了底层指针所指向的元素。类似于
void modifyElement(int* p) { |
三、总结
使用 Slice 应该尽量避免实际例子1 中的操作,尽量使用 s = append(s, x) 这样的形式。