go 深入剖析slice和array [精简版]

以前写过一篇关于 golang 中 slice & array 的介绍,但是我觉得写的太复杂了,很多特性是日常开发中用不到的,今天就来精简一下

本文就挑几个重要的部分来讲

1. Slice 的本质

  • slice 是一种特殊的数据结构,如下:

    1
    2
    3
    4
    5
    type slice struct {
    array unsafe.Pointer
    len int
    cap int
    }
  • 所以一般声明slice之后,使用 unsafe.Sizeof() 函数打印出来的大小应该是 24 (在64位机器上)

    1
    2
    slice1 := []int{1, 2, 3, 4, 5}
    fmt.Println("size of []int is : ", unsafe.Sizeof(slice1)) // 24

2. Array 不是指针

  • Go 语言的数组不同于 C/C++ 语言或者其他语言的数组,C/C++ 语言的数组变量是指向数组第一个元素的指针;
  • Go 语言的数组是一个值,Go 语言中的数组是值类型,一个数组变量就表示着整个数组。
  • 意味着 Go 语言的数组在传递的时候,传递的是原数组的拷贝。
  • 如果有必要可以传指针 *[4]int,不过感觉这样用的比较少,毕竟大多是用 slice 😂
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    func modifyArr(arr1 [4]int) {
    arr1[0] = 23
    fmt.Println("in modify arr func, arr = ", arr1) // in modify arr func, arr = [23 2 3 4]
    }

    func main() {
    arr1 := [4]int{1, 2, 3, 4}
    fmt.Println("before modify, arr = ",arr1) // before modify, arr = [1 2 3 4]
    modifyArr(arr1)
    fmt.Println("after modify, arr = ",arr1) // after modify, arr = [1 2 3 4]
    }
    ```

    ## 3. “切片”现有切片
    - 名字起得好,观众少不了 😂
    - 通过“切片”`现有切片或数组 (没错,数组也可以切切切)`来形成切片。 通过指定半开放范围来完成切片,其中两个索引用冒号分隔。 例如,表达式b [1:4]创建包括b的元素1到3的切片(得到的切片的索引将是0到2)。
    - 注意通过👆上面方法切出来的 `子切片` 和 `原切片` 共享同一个内存空间哦,下面仔细说说
    1. 下面的例子中,`b` 的 `len` 和 `cap` 都是6,`b1` 的 `len` 和 `cap` 分别是 2 和 5
    2. 这样就有一个特殊的情况:如果向 `b1` append 一个数据,由于 `cap(b1)=5`,这个时候是不会发生扩容的,因此 `b` 也会收到影响
    3. 像上面这样切出来的 `子切片` 和 `原切片`,任意一个发生扩容的话,就不会互相影响了
    - 感觉上面 `2` 中说的情况可以出一个面试题 😂

    ```go
    b := []byte{'g', 'o', 'l', 'a', 'n', 'g'} // len = 6, cap = 6
    b1 = b[1:4] // sharing the same storage as b, len = 2, cap = 5

copy

  • 如果不想 子切片原切片 互相作用的话,就直接使用 copy 即可
    1
    copy( destSlice, srcSlice []T) int
1
2
3
4
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

3. 切片的 nil 值

  • 感觉这一块也是知道就行,实际生产过程中涉及不到,面试可能会问(就是那种比较变态的面试官😂)
  • slice 有三种状态:零切片、空切片、nil切片。

零切片

  • 就是其元素值都是元素类型的零值的切片,如下所示:
    1
    2
    s := make([]int, 9, 10)
    fmt.Println(s) // 9 10 [0 0 0 0 0 0 0 0 0]

空切片

  • 就是数组指针不为nil,且 slice 的长度为0。
  • 空切片可以理解就是切片的长度为0,就是说 slice 没有元素。
  • 社区大多数解释空切片为引用底层数组为 zerobase 这个特殊的指针。但是从操作上看空切片所有的表现就是切片长度为0,如果容量也为零底层数组就会指向 zerobase ,这样就不会发生内存分配, 如果容量不会零就会指向底层数据,会有内存分配。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    var s []int
    s1 := make([]int, 0)
    s2 := make([]int, 0, 0)
    s3 := make([]int, 0, 100)

    arr := [10]int{}
    s4 := arr[:0]

    fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s))) // {0 0 0}
    fmt.Println(s) // []
    fmt.Println(s == nil) // true

    fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s1))) // {18438904 0 0}
    fmt.Println(s1) // []
    fmt.Println(s1 == nil) // false

    fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s2))) // {18438904 0 0}
    fmt.Println(s2) // []
    fmt.Println(s2 == nil) // false

    fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s3))) // {824634277888 0 100}
    fmt.Println(s3) // []
    fmt.Println(s3 == nil) // false

    fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s4))) // {824633835680 0 10}
    fmt.Println(s4) // []
    fmt.Println(s4 == nil) // false
  • 以上示例中除了 s 其它的 slice 都是空切片,打印出来全部都是 [],s 是nil切片下一小节说。
  • 要注意 s1 和 s2 的长度和容量都为0,且引用数组指针都是 18349960, 这点太重要了,因为他们都指向 zerobase 这个特殊的指针,是没有内存分配的。

nil切片

  • 就是引用底层数组指针为 nil 的 slice。
  • 什么是nil切片,这个名字说明nil切片没有引用任何底层数组,底层数组的地址为nil就是nil切片。
  • 上一小节中的 s 就是一个nil切片,它的底层数组指针为0,代表是一个 nil 指针。

总结

  • 操作上零切片、空切片和正常的切片都没有任何区别,但是nil切片会多两个特性,一个nil切片等于 nil 值,且进行 json 序列化时其值为 null,nil切片还可以通过赋值为 nil 获得。