Slice

从你会的东西开始

站在我的角度来看, 正是因为Slice太好用了, 所以很多人最后忘记了有数组的存在了, 的确是啊, 大家都搞[]string+append的方式来用, 好像确实找不到什么理由来使用数组. 如何创建出slice&array? 指明/不指明size就可以:

  • 创建一个数组:

    • arr := [3]string{}

    • arr := [...]string{}

  • 创建一个切片:

    • sli := []string{}

    • sli := make([]string, 10)

  • 数组变切片:

    • sli := arr[:]

slice(指针)/array(实体)

我们从最简单的开始想象, 如果想要存一组元素, 最简单的方式是什么? 是不是就是搞一小块内存出来存着就好咯? 没有指针, 没有滑头, 就一组数字, 存就完事儿

也正是因为数组 = 实体, 没有指针等滑头, 结合Golang函数传值(复制)的特点, 我们也能想象出上面会发生什么, 一个数组在传递的过程中, 他的值被拷贝了一份

与之相对的是slice并没有实体, 它只有一个指针, 因此在函数传递参数的时候传指针, 修改的是同一份内容.

切片到底是什么? 不欢迎玄学!

来玩个游戏, 我们先创建一个数组, 然后通过"[:]"的方式创建出Slice, 好玩的:

  • 那么他俩的地址是? 是同一个地址

  • 那么修改数组, 对应的slice会怎样? 会跟着一起改

狗吧? 现在你知道为什么形参为[3]int{}的函数不能接受[]int{}的实参了吧? 因为一个是指针一个是实体嘛(偶偶, 这下我完全搞懂了!完全没懂)

slice的初始化

继续玩! 之前在写Map的时候这么玩过一次, 这招能帮我们看到到底执行了什么函数, OK, 这里的make被翻译成了runtime.makeslice:

make函数被编译器翻译成了makeslice函数,这个函数通过malloc, 以unsafe.Pointer的方式, 返回一个指向对应内存段的指针 (简单的理解就是void*, 后面再cast成实际的类型)

我也不是嘴嗨, 上面这就是runtime里的定义, 想要创建一个slice需要先预估大小后创建一个数组指针(unsafe), 然后塞到slice结构体里才能变成你天天用的slice. 平时的len,cap什么的都有在搞吧?(压力马斯内!) 原理就是检查slice结构体里的变量

数组是不是一无是处...

也不是一无是处, 数组的一个特点就是定长, 这使得编译阶段的越界检查成为可能, 尝试通过go build(不运行, 只编译)一下上面的片段, 你会发现连编都编不过

那么Slice呢, slice因为是指针也不定长因此编译期的越界管不到, 怎么办呢? 只能运行期内panic咯

Append Slice

思考以下问题: append返回什么? 答案很有趣, slice因为是指针, 所以也要考虑深拷贝浅拷贝问题, 这道题的答案是: 取决于你返回值是不是覆盖变量

  • 如果覆盖, 则:

    • 判断slice下数组容量是否足够, 是否需要扩容(growslice)

    • 然后将这个数组重新给slice

  • 如果不覆盖, 则:

    • 判断slice下数组容量是否足够, 是否需要扩容(growslice)

      • 如果需要扩容, 并分配一个新内存[深拷贝]

      • 如果不需要扩容, 则新老两个指向同一个数组[浅拷贝]

初始化的slice只有1容量, 按照加倍增长的原则, 变成2/变成4, 每次扩容, 底层的那个数组就会重新分配一次, 因此地址也在跟着变, 最后在append2/3的时候因为容量够了, 所以不会重新分配内存

那么回到一个经典问题, 如果我改slice, new_slice会不会一起改呢? 看看上面两种情况下, 两个slice的地址.

  • 扩容了分配了新数组, 不会一起改, 底层数组都不一样么

  • 没扩容也没分配新数组, 一起改, 因为指向的是同一个底层数组

非常鬼畜对吧? 怎么办呢? 我现在就是想要一个新slice, 不要跟之前的有任何关联, 请指明使用深拷贝: make+copy

Last updated

Was this helpful?