GO 切片实力踩坑

1,079 阅读4分钟

概述

GO 语言的切片这两天用了用, 可以支持切割数组的中间部分. 但今天使用中, 出了 bug, 查了半天, 发现是切片的问题, 简单写个 demo 复现当时的情况:

package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	b := a[2:4]
	b[0] = 9
	fmt.Println(a)
}

你以为输出的是什么? 来, 看结果:

[1 2 9 4 5]

懵没懵?? 这是怎么回事呢?

(我用个语言怎么老踩坑, 笨的一X)

解惑

看这段 GO 代码的输出, 我们在修改b数组第一个元素值的时候, a数组的第三个元素修改了, 这两个有什么联系吗? 仔细看, b数组在切的时候, 切的不就是a数组第三第四的元素吗? 如此看来, b[0] 不正对应 a[2] 吗?

大胆假设: **GO 中对数组进行切割, 并不会切一个新的数组出来, 而是仍然使用原数组, 只是修改下数组的首地址和长度. **

验证:

package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	b := a[2:4]
	b[0] = 9
	fmt.Printf("%p\n", &a[2])
	fmt.Printf("%p\n", &b[0])
}

打印出来的地址完全一致, 印证了之前的猜想, 果然是一个数组. 同时修改a数组, 也会影响到b数组.

那可不可以对b数组越界访问, 访问a数组的值呢? 不行, GO 会对数组进行越界检查.

查看文档后发现, GO 切片的内部实现是这样的包含了三个字段. 其中各字段含义如下:

  1. 数组首地址指针: 指向底层数组的首地址(这个是真正的数组)
  2. 数组长度: 数组当前已经使用的长度
  3. 数组容量: 数组已分配内存的总长度, 比数组长度多出的部分, 是占用内存还没有使用的.

如此看来, 对其进行切割, 并不会整个复制, 对于大切片的操作就显得很友好了, 毕竟共享底层数组, 只需要创建很少的数据就可以了. 只是要注意数组的同步修改问题.

这么看来, 貌似也可以解释为什么叫切片了. **切片就是将底层的数组切出一部分来, 而不会创建新的数组. **

切片是有容量的, 那上面的切片b的容量是多少呢? 我看了一下: 长度是2, 容量是3.

GO 的切片在容量足够的时候, 是不会动态扩容的. (扩容会创建更大容量新的数组并复制原数组数据). 那也就是说, 如果我向b追加数据, 就可以影响到原数组的后面的数据了??

试一发:

package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	b := a[2:4]
	b = append(b, 10)
	fmt.Println(b)
	fmt.Println(a)
}

果然, 容量允许的话, 追加操作使用的仍然是原始数组.

所以: 切片的容量其实是底层数组的容量

同时, 有了之前对 GO 的了解, 知道 GO 所有的函数都是以传递副本值的方式进行, 传递切片也一样, 而切片的结构体包含(数组指针, 长度, 容量)三个元素, 底层数组并不属于值本身, 所以切片在函数间传递的复制成本很小, 而且函数对切片的修改也会反应到底层数组上. 同理可得, 如果在函数中对切片执行了扩容操作, 那改动就不会影响原数据, 因为扩容后操作的是新的数组了.

OK. 切片到这里就结束了, 简单说就是数组上面再套一层. 切片的切片共享底层数组.

最后说一句, GO 创建数组和切片的方式(数组和切片是不同的数据结构):

// 方括号为空, 创建的是切片类型
a := []int{1, 2}
// 方括号指定长度, 创建的是真正的数组类型
b := [2]int{}

总结

至此, 对 GO 的切片有了全新的认识. 在使用切片的时候, 需要特别注意, 切片的截取与原对象共享底层数组, 在数据修改时要特别注意.

如果需要一个安全的可修改的切片, 可以使用copy函数复制一个全新的数组出来, 与原数组分离就可以了.