xiaohanliang
Golang
Golang
  • review
  • DATA STRUCT
    • Slice
    • Map
    • Lock
    • Chanel
    • Pool
    • Interface
  • SCHEDULE
    • 为什么要设计Go协程
    • GMP都是干什么的
    • 一次完整的调度
    • 什么是G栈
    • P&M剥离
    • 常见的暂停操作
  • OTHERS
    • GC介绍
    • 内存介绍
    • 内存分析
Powered by GitBook
On this page
  • 从你会的东西开始
  • slice(指针)/array(实体)
  • 切片到底是什么? 不欢迎玄学!
  • slice的初始化
  • 数组是不是一无是处...
  • Append Slice

Was this helpful?

  1. DATA STRUCT

Slice

PreviousreviewNextMap

Last updated 4 years ago

Was this helpful?

从你会的东西开始

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

  • 创建一个数组:

    • arr := [3]string{}

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

  • 创建一个切片:

    • sli := []string{}

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

  • 数组变切片:

    • sli := arr[:]

slice(指针)/array(实体)

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

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

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

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

func main() {
        arr := [5]int{1,2,3,4,5}
        sli := arr[:]
        fmt.Printf("Addr[arr]=%p \n",arr[0])
        fmt.Printf("Addr[sli]=%p \n",sli[0])        

        arr[2] = 69
        println(sli[2])
}
/*
        输出:
                Addr[arr] = 0xc0000a4000
                Addr[sli] = 0xc0000a4000
                slice[2]  = 69
*/

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

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

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

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

slice的初始化

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

func makeslice(...) unsafe.Pointer {
        return malloc(...)
}

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

// runtime/slice.go+13
type slice stuct {
    array unsafe.Pointer
    len   int
    cap   int
}

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

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

func main() {
        arr := [5]{1,2,3,4,5}
        _ = arr[10]
}

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

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

Append Slice

slice = append(slice,v)         // 覆盖, 追加
a_new_slice := append(slice,v)  // 不覆盖, 覆盖 + 深拷贝

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

  • 如果覆盖, 则:

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

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

  • 如果不覆盖, 则:

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

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

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

// 覆盖的情况
slice := []int{0}       // &slice[0] = 0xc000018110 cap=1
slice = append(slice,1) // &slice[0] = 0xc000018130 cap=2
slice = append(slice,2) // &slice[0] = 0xc000014240 cap=4
slice = append(slice,3) // &slice[0] = 0xc000014240 cap=4

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

// 不覆盖 + 需要扩容
slice := []int{1,2,3}           // &slice = 0xc0000ae020
a_new_slice := append(slice,4)  // &new   = 0xc0000ac030

// 不覆盖, 不扩容
slice := make([]int, 0, 10).    // &slice = 0xc00001a0a0
a_new_slice := append(slice,4)  // &new.  = 0xc00001a0a0

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

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

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

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