深入了解slice

前言

最近进了煎鱼大佬的群,看到群里在讨论slice的常见的坑以及面试官喜欢问的相关问题,突然意识到自己原来还有这么多知识盲区有待消除。本文主要是整理下对slice的认识和梳理相关知识点,如果哪里写的不对或有不同见解,欢迎指出哈~

基本认识

slice是一个轻量级的数据结构,提供访问数组子序列部分或全部元素的功能,通常写作:[]T,其中T代表元素的类型。跟数组的重要区别是其长度是动态变化的,正因为这个特点,slice的出场率要远远高于数组。slice源码位于:SDK/runtime/slice.go,其数据结构:

type slice struct {
    array unsafe.Pointer 
    len   int
    cap   int
}
  • 一个slice由三部分组成:指针、长度、容量,注意的是该指针是指向slice的第一个元素对应的底层数组元素的地址(不一定是数组的第一个元素哦)。
  • 多个slice是可以共享同个底层数组的,且引用的数组可能会重叠(用的时候要考虑他们的相互影响问题,下面会提到),这个取决于具体的使用场景。
  • slice 通过内建的append函数向slice添加元素,无直接删除slice元素的api(可通过切片操作实现)
  • 不能通过 == 操作符比较两个slice是否含有完全相等的元素 ,不过可以使用reflect.DeepEqual方法实现

切片相关操作

  1. 使用make和append基本操作
a := make([]int,0,3)
fmt.Println(a)
b := make([]int,3,3)
fmt.Println(b)
b = append(b,3)
fmt.Println(b)
//append(b,3) 这种写法无法通过编译,使用b=append(b,3)方式是需要更新old slice的len和cap,与底层数组保持一致
//output:
//[]
//[0 0 0]
//[0 0 0 3]
  1. 切片操作符:
  • 遵循"左闭右开"区间原则
  • slice.len = right - left, 如果left省略代表0,right省略代表len. 例如: b.len = 4 - 1 = 3
  • 新cap=slice的起始位置到底层数组的结尾位置,即 slice.cap = old.cap - slice[0]'s index,例如下面的slice d,d.cap = 4 - 2 = 2
var a = [4]string{"1", "2", "3", "4"} // a is an array
b := a[1:]
fmt.Println(len(b), cap(b))
var c = []int{1, 2, 3, 4} // c is a slice
d := c[2:4]
fmt.Println(len(d), cap(d))
//output:
//3 3
//2 2
  1. 使用copy函数,直接copy一份完全一样且独立的slice
var b = []int{1,2,3,4,5}
s1 := make([]int,2,2)
copy(s1,b[:2])
s1 = append(s1,10)
fmt.Println(s1)
fmt.Println(b)
//output:
//[ 1 2 10]
//[ 1 2 3 4 5]

顺便提一下,使用copy的时候,目的slice的len如果等于0则会copy失败,理想情况下len等于需要copy的长度即可,例如:

a := []int{1,2,3}
b := make([]int,3,3)
copy(b,a)
fmt.Println(b)

b1 := make([]int,0,3)
copy(b1,a)
fmt.Println(b1)
//output:
//[1 2 3]
//[]
  1. 3种声明空slice方式
a := []int(nil)
fmt.Println(a == nil, len(a), cap(a))
b := []int{}
fmt.Println(b == nil, len(b), cap(b))
var c []int
fmt.Println(c == nil, len(c), cap(c))
//output:
//true 0 0
//false 0 0
//true 0 0

底层实现机制导致的易错点

直接看代码:

func main()  {
    var a = []uint8{1,2,3,4,5,6}
    b := a[0:2]
    b = append(b,uint8(10))
    fmt.Println(b)
    fmt.Println(a)
    /*
    output:
    [1 2 10]
    [1 2 10 4 5 6]
    */
    a = []uint8{1,2,3,4,5,6}
    c := a[:]
    c = append(c,uint8(10))
    fmt.Println(c)
    fmt.Println(a)
    /*
    output:
    [1 2 3 4 5 6 10]
    [1 2 3 4 5 6]
    */
}

a和b共享了底层数组,b 的ptr指向的是a底层数组的第一个元素,此时b.len=2,b.cap=cap(a),b在append时,因为 len(b) + 1 < cap(5),无需扩容,所以直接b[2]=10进行了赋值,导致a[2]的元素也被更改了。这个问题可以使用copy来避免:

//b := a[0:2]
b := make([]uint8,2,cap(a))
copy(b,a[:2])
b = append(b, uint8(10))
fmt.Println(b)
fmt.Println(a)
//output:
//[1 2 10]
//[1 2 3 4 5 6]

slice扩容机制

slice扩容机制是面试官比较偏好的问题之一,了解slice的扩容机制可以帮助我们在coding的时候写出带有合理容量的slice,提升代码质量,也是衡量对源码熟悉程度的方式之一(为什么?因为slice使用场景实在太多了)。

  1. 什么时候(什么条件)会扩容?
    先来看下简单例子:
s1 := make([]int,0,2)
fmt.Printf("len:%d \tcap:%d\n",len(s1),cap(s1))
s1 = append(s1,[]int{3,2,1}...)
fmt.Printf("len:%d \tcap:%d\n",len(s1),cap(s1))
//output:
//len:0   cap:2
//len:3   cap:4

本来想着直接查看append函数源码一探究竟的,但append是内建函数,通常由编译器做了特殊处理的,无法直观看到其源码,stackoverflow有相关解释。Go语言圣经中有简单描述了下append的大概逻辑,感兴趣可前往阅读。是否要扩容取决与len是否要大于容量了,如果是即意味要扩容,否则直接 oldSlice[len(oldSlice)] = elem 赋值更新即可。

  1. 扩容机制的实现?
    关于扩容,可能很多的一个说法就是:"len小于1024,每次扩容cap *= 2,否则 cap *= 1.25",看了很多博客分析,其实这样理解是错的。可以参照这篇文章去理解: 数组与切片 ,总结下来就是:每次append一个元素的场景下,前半句成立,但append多个元素的情况下整个说法都是不成立的。所以,还有人直接照搬上面那句笼统的说法的,你可以用上面那篇博客反驳他!
    截取部分源码简单分析:
// et 是要添加的元素的类型的一个抽象,old是原slice,cap是期望的能满足容量需求的最小值
func growslice(et *_type, old slice, cap int) slice {
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
...
    var p unsafe.Pointer
    if et.ptrdata == 0 {
        p = mallocgc(capmem, nil, false)
        // The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
        // Only clear the part that will not be overwritten.
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
        p = mallocgc(capmem, et, true)
        if lenmem > 0 && writeBarrier.enabled {
            // Only shade the pointers in old.array since we know the destination slice p
            // only contains nil pointers because it has been cleared during alloc.
            bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
        }
    }
    memmove(p, old.array, lenmem)

    return slice{p, old.len, newcap}

关于里面涉及的内存对齐、内存分配等,目前能力还没到能理解这些东西的层次,先知道有这么个东西,日后再探讨,目前先不做讨论。

你可能感兴趣的:(深入了解slice)