概述
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 切片的内部实现是这样的包含了三个字段. 其中各字段含义如下:
- 数组首地址指针: 指向底层数组的首地址(这个是真正的数组)
- 数组长度: 数组当前已经使用的长度
- 数组容量: 数组已分配内存的总长度, 比数组长度多出的部分, 是占用内存还没有使用的.
如此看来, 对其进行切割, 并不会整个复制, 对于大切片的操作就显得很友好了, 毕竟共享底层数组, 只需要创建很少的数据就可以了. 只是要注意数组的同步修改问题.
这么看来, 貌似也可以解释为什么叫切片了. 切片就是将底层的数组切出一部分来, 而不会创建新的数组.
切片是有容量的, 那上面的切片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
函数复制一个全新的数组出来, 与原数组分离就可以了.