golang slice

关于golang slice有很多大神写了很多文章,阐述了slice的底层实现和使用中注意点.这篇文章是我参考https://www.calhoun.io/why-are-slices-sometimes-altered-when-passed-by-value-in-go/ 加了一些自己的总结。

  • slice的实现原理,理解slice的实现原理是理解slice的一些很奇葩注意点的关键.一图胜千言.
image.png

如图所示slice的底层是一个array. slice是一个结构里面有2属性 len 和cap分别表示当前使用的个数和可以容纳的个数.

  • example1
func main() {
    var s []int
    for i:=1;i<7;i++{
        s=append(s, i)
        //fmt.Printf("%p\n",s)
    }
    s2:=s[:]
    s1:=s[:]
    s1=append(s1,100)
    fmt.Printf("s1:%p len:%d,cap:%d\n",s1, len(s1), cap(s1))
    s2=append(s2, 100,200,300)
    fmt.Printf("s:%p len:%d,cap:%d\n",s, len(s), cap(s))
    fmt.Printf("s2:%p len:%d,cap:%d\n",s2, len(s2), cap(s2))
}

如上代码,先声明一len 0 cap 0的slice 然后往里面append元素.s的len和cap大概如下变化

len cap  s[i]         s 
0     0    0      
1     1    1       0xc42001a050
2     2    2       0xc42001a070
3     4    3       0xc420016140
4     4    4       0xc420016140
5     8    5       0xc420018100
6     8    6       0xc420018100

这个变化就是当cap不够时重新分配一个新的cap为当前cap的2倍的array. 注意右边的s指向的地址变化,每当重新分配新的array的时候s 指向的地址就会变化。

回到上面的main代码,如果使用切片生成一个新的slice s1 s2,注意这里s1 append 了一个元素,s2 append 3个元素.

s1:0xc420018100 len:7,cap:8
s:0xc420018100 len:6,cap:8
s2:0xc42008c000 len:9,cap:16

Process finished with exit code 0

这里s1指向的地址和s一样,只是len加1,因为s1只是append了一个元素,没有超出s的cap
s2 指向的地址和s不一样了,append3个元素超出了s的cap所以重新分配了一个数组

  • example2
func main() {
    var s []int
    for i:=1;i<7;i++{
        s=append(s, i)
    }
    fmt.Printf("s point:%p  s len:%d s cap:%d %v\n",s,s, len(s), cap(s))
    reverse1(s)
    fmt.Printf("s point:%p  s len:%d s cap:%d %v\n",s,s, len(s), cap(s))
}
func reverse1(s[]int){
    for i,j:=0,len(s);i<j;i++{
        j=len(s)-(i+1)
        s[i],s[j]=s[j],s[i]
    }

}

func reverse2(s[]int){
    s=append(s, 8888,7777)
    for i,j:=0,len(s);i<j;i++{
        j=len(s)-(i+1)
        s[i],s[j]=s[j],s[i]
    }

}
func reverse3(s[]int){
    s=append(s, 8888,7777,1000)
    for i,j:=0,len(s);i<j;i++{
        j=len(s)-(i+1)
        s[i],s[j]=s[j],s[i]
    }

}

reverse1的输出

s point:0xc420018100  s len:[1 2 3 4 5 6] s cap:6 8
s point:0xc420018100  s len:[6 5 4 3 2 1] s cap:6 8

reverse2 的输出

s point:0xc420018100  s len:[1 2 3 4 5 6] s cap:6 8
s point:0xc420018100  s len:[7777 8888 6 5 4 3] s cap:6 8

reverse3 的输出

s point:0xc42008a040  s len:[1 2 3 4 5 6] s cap:6 8
s point:0xc42008a040  s len:[1 2 3 4 5 6] s cap:6 8

注意调用这3个函数后s的变化的差别
1. reverse1 只是将s中的元素顺序逆转了一下,只改变了元素的值。没有改变len cap.
2. reverse2 中给s append了一个元素,并且append之后的slice长度还是小于cap所有没有重新分配数组。注意这里append元素是不会改变main里面的s的len和cap的.
3 .reverse3 中append 3个元素超过cap,重新分配底层数组,所以reverse中的s2 指向的地址和main中的s分别指向2个不同的数组。所以逆转操作是发生在另外一个数组元素上而不是main的传进来的slice对应的数组上。

  • golang中的值传递
    golang官方中明确指出golang中的所有的函数传参都是值传递.什么意思,即所有的传进函数方法的参数都是原来变量的拷贝.包括slice也是,有人说slice map channel是引用传递,其实是错误的,但是可以在函数中修改传入的参数的内容啊。
    如同刚才的slice的例子,传进来的s只是main总的一份拷贝, reverse1 中的s和main中的s得内存地址是不一样的。
s point:0xc42008a040  s len:[1 2 3 4 5 6] s cap:6 8
0xc420096020
0xc420096080
s point:0xc42008a040  s len:[6 5 4 3 2 1] s cap:6 8

只是slice里面指向的底层数组地址是同一个,所以reverse里面不管怎么改动,main里面的len cap pointer 都是不会被改变的。你只能改变pointer指向的数组的元素值

  • 相似的类型
type A struct {
  Ptr1 *B
  Ptr2 *B
  Val B
}

type B struct {
  Str string
}

func main() {
  a := A{
    Ptr1: &B{"ptr-str-1"},
    Ptr2: &B{"ptr-str-2"},
    Val: B{"val-str"},
  }
  fmt.Println(a.Ptr1)
  fmt.Println(a.Ptr2)
  fmt.Println(a.Val)
  demo(a)
  fmt.Println(a.Ptr1)
  fmt.Println(a.Ptr2)
  fmt.Println(a.Val)
}

func demo(a A) {
  // Update a value of a pointer and changes will persist
  a.Ptr1.Str = "new-ptr-str1"
  // Use an entirely new B object and changes won't persist
  a.Ptr2 = &B{"new-ptr-str-2"}
  a.Val.Str = "new-val-str"
}

其实slice是类似一个上面结构的类型

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

上面的例子里面a作为值传递传给demo函数.demo函数持有的实际上是a的一份拷贝.修改这个拷贝里面的属性值是不会影响main里面的a的值,但是注意和slice类似,因为A是一个拥有一个指针属性的,虽然无法修改ptr1的值但是可以修改ptr1指针指向的地址的内容。所以ptr1 ptr2 中的地址还是没有变,但这个地址指向的内存中的内容被修改了。类似于slice里面pointer的值没有变,但是数组元素被修改了。

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

推荐阅读更多精彩内容

  • 首先我们来看段代码的输出 输出的结果是 append的值5并没有输出,那么究竟是s0并不等价于s[0],还是有其他...
    Love语鬼阅读 4,092评论 0 4
  • 这部分: 有时候可能需要使用一些比较tricky的技巧,比如利用make弄一块内存自己管理,或者用cgo之类的方式...
    HuJay阅读 605评论 0 0
  • array 和slice都是数组,前者固定大小,值类型;后者可以动态变更,引用类型。再次强调一遍,array在go...
    大漠狼道阅读 1,006评论 0 0
  • 相比于 c/c++,golang 的一个很大的改进就是引入了 gc 机制,不再需要用户自己管理内存,大大减少了程序...
    hatlonely阅读 542评论 0 3
  • 婚前,梁思成问林徽因:“有一句话,我只问这一次,以后都不会再问,为什么是我?” 林徽因答:“答案很长,我得用一生去...
    桔穿晓知阅读 623评论 2 4