深入了解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}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容

  • 1. slice的结构 slice是值类型 slice类型声明后类似于:var arr slice 而非 var ...
    Lucas_Ye阅读 458评论 0 2
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,712评论 2 59
  • 实验一:我们都知道,数组声明以后,数组的地址和数组内元素的地址是固定的。而slice则不然,如果修改slice1的...
    观镜人阅读 242评论 0 0
  • 切片定义 切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持...
    玖零儛阅读 391评论 0 1
  • [TOC] Array Array赋值:会复制所有元素 函数传递:会复制所有元素,如果要修改Array的值,传递指...
    cdz620阅读 249评论 0 0