golang数组和切片的底层实现区别和常见的坑

数组是由相同类型元素的集合组成的数据结构,计算机会为数组分配一块连续的内存来保存其中的元素,我们可以利用数组中元素的索引快速访问特定元素。goalng中的数组在定义时必须指定长度,创建之后长度是不可变的。因为在数组创建过程中,golang会根据定义的长度去申请连续的内存空间。与其他编程语言一样,数组的指针指向数组开头元素。

golang的切片类型是基于数组实现的,可以理解为一个管理数组自动扩容的结构,具体的结构体定义如下:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

array是一个指向数组结构的指针,len和cap字段用于维护数组的长度和容量。当我们定义一个切片类型时,切片的初始容量为0,当我们逐渐append变量时,切片会依据某种策略进行扩容。具体的扩容策略比较复杂,具体可以查看go/src/runtime/slice.go的源码。

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
            }
        }
    }
    ......
}

简单来说:如果需要的cap > oldcap*2 ,就直接分配需要的cap。否则,如果oldcap< 1024, 直接加倍。如果old <= 1024,反复增加25%。直到足够存储全部内容。实际上的逻辑更加复杂,最终的cap还需要根据数据类型所占的空间进行调整,具体可以参考源码。接下来,通过如下实验验证两种简单的情况。首先是不指定默认长度和容量时。

func TestNewSlice(T *testing.T){
    var slice []int
    fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))

    for i:=0;i<100;i++{
        slice = append(slice, i)
        fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))
    }
}

实验结果如下:

slice len = 0        slice cap = 0
slice len = 1        slice cap = 1
slice len = 2        slice cap = 2
slice len = 3        slice cap = 4
slice len = 4        slice cap = 4
slice len = 5        slice cap = 8
slice len = 6        slice cap = 8
slice len = 7        slice cap = 8
slice len = 8        slice cap = 8
......

接下来是通过make指定初始的长度和容量时。

func TestMakeSlice(T *testing.T){
    var slice = make([]int,0,10)
    fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))

    for i:=0;i<100;i++{
        slice = append(slice, i)
        fmt.Printf("slice len = %d \t\t slice cap = %d\n",len(slice),cap(slice))
    }
}

实验结果如下:

slice len = 0        slice cap = 3
slice len = 1        slice cap = 3
slice len = 2        slice cap = 3
slice len = 3        slice cap = 3
slice len = 4        slice cap = 6
slice len = 5        slice cap = 6
slice len = 6        slice cap = 6
slice len = 7        slice cap = 12
......

因此,如果我们知道切片大致需要的容量时,最好通过 make方法,指定cap值。这样可以有效避免数组的频繁扩容。从而避免切片在扩容时导致的性能损失。这部分损失包括扩容时重新申请内存、数据的拷贝以及后续的垃圾回收。

golang数组和切片的区别除了体现在是否支持扩容外,还体现在传值操作上。具体我们通过如下实验来说明。

func TestSliceObj(t *testing.T) {

    var a1 = [3]int{1,2,3}
    fmt.Printf("the a1 = %v \t\t  a1 ptr = %p \t\t a1[0] ptr = %p\n",a1,&a1,&a1[0])

    a2 := a1
    a1[0] = 10
    
    fmt.Printf("after change the a1 = %v \t\t  a1 ptr = %p \t\t a1[0] ptr = %p\n",a1,&a1,&a1[0])
    fmt.Printf("after change the a2 = %v \t\t  a2 ptr = %p \t\t a2[0] ptr = %p\n",a2,&a2,&a2[0])
    
    var s1 = []int{1,2,3}
    fmt.Printf("the s1 = %v \t\t  s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])

    s2 := s1
    s1[0] = 10

    fmt.Printf("after change the s1 = %v \t\t  s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])
    fmt.Printf("after change the s2 = %v \t\t  s2 ptr = %p \t\t s2[0] ptr = %p\n",s2,&s2,&s2[0])
}

实验结果如下:

=== RUN   TestSliceObj
the a1 = [1 2 3]          a1 ptr = 0xc0000ca0a0          a1[0] ptr = 0xc0000ca0a0
after change the a1 = [10 2 3]        a1 ptr = 0xc0000ca0a0          a1[0] ptr = 0xc0000ca0a0
after change the a2 = [1 2 3]         a2 ptr = 0xc0000ca0e0          a2[0] ptr = 0xc0000ca0e0
the s1 = [1 2 3]          s1 ptr = 0xc0000b40a0          s1[0] ptr = 0xc0000ca140
after change the s1 = [10 2 3]        s1 ptr = 0xc0000b40a0          s1[0] ptr = 0xc0000ca140
after change the s2 = [10 2 3]        s2 ptr = 0xc0000b40e0          s2[0] ptr = 0xc0000ca140
--- PASS: TestSliceObj (0.00s)

通过实验结果我们发现,对于数组array来说,数组的指针和数组第一个元素的第一个指针是相同的,说明数组指针确实指向数组第一个元素地址。接着我们将a1值赋值给a2并修改a1的值。我们发现a1和a2分别指向不同的内存空间,同时对a1的修改不会影响a2。说明赋值过程是值传递,在赋值过程中会重新申请一块空间。

对于切片slice来说,切片指针指向slice struct的位置,而切片第一个元素的地址位才是底层数组真正的地址位。然后我们将s1赋值给s2,我们发现是s1和s2指针分别指向两个不同的空间,说明该赋值过程同样也是值传递,即当前内存中存在两个slice结构体对象,分别为s1和s2。与数组不同的是,两个切片所对应的底层数组是同一个。当我们修改s1的值之后,s2也同样发生了变化。

那么这是不是就说明,golang中切片类型的赋值是指针复制或者说是浅拷贝呢?

答案是否定的,接下来介绍一个golang切片使用过程中常见的坑。首先做一组实验。

func TestAppendSlice(T *testing.T){
    var s1 = []int{1,2,3,4}
    fmt.Printf("the s1 = %v \t\t  s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])

    s2 := s1
    s1 = append(s1, 5)

    fmt.Printf("after change the s1 = %v \t\t  s1 ptr = %p \t\t s1[0] ptr = %p\n",s1,&s1,&s1[0])
    fmt.Printf("after change the s2 = %v \t\t  s2 ptr = %p \t\t s2[0] ptr = %p\n",s2,&s2,&s2[0])
}

实验结果如下:

=== RUN   TestAppendSlice
the s1 = [1 2 3 4]        s1 ptr = 0xc00000c0e0          s1[0] ptr = 0xc000018200
after change the s1 = [1 2 3 4 5]         s1 ptr = 0xc00000c0e0          s1[0] ptr = 0xc00001a180
after change the s2 = [1 2 3 4]           s2 ptr = 0xc00000c120          s2[0] ptr = 0xc000018200
--- PASS: TestAppendSlice (0.00s)

通过实验结果我们发现,当我们将s1赋值给s2之后,再修改s1,s2并未像之前一样也发生变化。另外我们发现s2对应的底层数组与s1是相同的,这和之前的实验结论一致。不同之处在于,s1在修改后指针指向的底层数组地址发生了变化,这是因为append操作恰好出发了一次数组扩容,从而导致切片重新申请了一块连续地址。

因此在使用切片时一定注意这一点,尤其是当发生参数传递时,需要确定自己期待的是值传递还是指针传递。否则可能会遇到难以解释的bug。

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

推荐阅读更多精彩内容

  • Slice和数组概念定义以及区别 数组 数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素...
    亦一银河阅读 552评论 0 0
  • 数组是指一系列同一类型的数据集合。数组中包含的每个数据被称为数组元素,数组中元素的个数,称为数组的长度。 数组的创...
    水无寒阅读 1,170评论 0 0
  • 数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。  数组的长度在声明它的时候...
    one_zheng阅读 1,174评论 0 3
  • 数组的声明 变量的声明已经讲过啦,不熟悉的可以看第二章 数组的遍历 两种方法,一种传统的下标遍历一种上一章讲到的r...
    神奇大叶子阅读 357评论 0 0
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,662评论 2 7