go切片与数组的关系—修改切片导致数组被修改的问题

2020-10-29更新
  "切片是指向数组的指针"这句话是不对的。切片就是切片,有自己的属性和方法,只是借用了数组来存储实际的数据。
切片的数据结构大概为

type slice struct {
    point int
    len int
    cap int
}

即,管理一个数组上以point指向元素的数组下标为起点,加上len长度为终点,最大扩容到cap长度为终点的一段数据。
举例:

func main() {
    D := [10]int{0,1,2,3,4,5,6}
    A := D[0:3:3]
    B := D[2:5:9]
    C := D[4:7]

    fmt.Printf("A: %v\n", A)
    fmt.Printf("B: %v\n", B)
  fmt.Printf("C: %v\n", C)
  fmt.Printf("D: %v\n", D)
}

注意:

    1. 数组没有指定值的位置会用默认值填充,此处即为0
    1. 创建切片时第三个参数表示切片容量到数组的哪个索引位置,没有就默认到数组末尾,这个下面有用处,所以分三种设置做对比
    1. 从数组取切片时是前闭后开区间,与通常规定操作一致。

运行输出:

A: [0 1 2]
B: [2 3 4]
C: [4 5 6]
D: [0 1 2 3 4 5 6 0 0 0]

修改切片内容如下:

func main() {
    D := [10]int{0,1,2,3,4,5,6}
    A := D[0:3]
    B := D[2:5]
    C := D[4:7]
    A[2] = 999
    B[2] = 666

    fmt.Printf("A: %v\n", A)
    fmt.Printf("B: %v\n", B)
    fmt.Printf("C: %v\n", C)
    fmt.Printf("D: %v\n", D)
}

运行输出:

A: [0 1 999]
B: [999 3 666]
C: [666 5 6]
D: [0 1 999 3 666 5 6 0 0 0]

可以看出所有修改都同步了,并且体现在数组D上。
再来看Append操作。Append是扩展切片的长度,但是如果长度超过了预设的容量,就需要换一个底层数组。看下面的程序:

func main() {
    D := [10]int{0,1,2,3,4,5,6}
    A := D[0:3:3]
    B := D[2:5:7]
    C := D[4:7]
    A = append(A, 333)
    B = append(B, 666)
    C = append(C, 999)

    fmt.Printf("A: %v\n", A)
    fmt.Printf("B: %v\n", B)
    fmt.Printf("C: %v\n", C)
    fmt.Printf("D: %v\n", D)
}

运行输出:

A: [0 1 2 333]
B: [2 3 4 666]
C: [4 666 6 999]
D: [0 1 2 3 4 666 6 999 0 0]

可以看到A因为预设了[0:3:3]的原因,容量只有3,当前已满,再增加一个333,就切换了新的数组,所以A的修改只体现在自身,对B、数组D都没有影响。
而B的容量为7,C的容量为5,都有空间,所以修改体现在了数组D上。
将切片A扩展到容量4,但是增加两个元素:

func main() {
    D := [10]int{0,1,2,3,4,5,6}
    A := D[0:3:4]
    B := D[2:5:7]
    C := D[4:7]
    A = append(A, 333)
    A = append(A, 332)
    B = append(B, 666)
    C = append(C, 999)
    fmt.Printf("A: %v\n", A)
    fmt.Printf("B: %v\n", B)
    fmt.Printf("C: %v\n", C)
    fmt.Printf("D: %v\n", D)
}

运行输出:

A: [0 1 2 333 332]
B: [2 333 4 666]
C: [4 666 6 999]
D: [0 1 2 333 4 666 6 999 0 0]

可以看到添加333时还没有超出切片A的容量,所以333还在数组D上做修改,而添加332时已经超出了A的容量,A换了一个新的数组(现有数据0、1、2、333复制过去),并且在新数组添加332,而不影响原来的数组D。
为什么强调不要把切片理解为数组的指针呢?这里还有一个非常重要的问题。看代码:

func main() {
    slice := make([]int, 2, 3)
    for i := 0; i < len(slice); i++ {
        slice[i] = i
    }
    fmt.Printf("slice: %v, addr: %p \n", slice, slice)
    changeSlice(slice)
    fmt.Printf("slice: %v, addr: %p \n", slice, slice)
}
func changeSlice(s []int){
    s = append(s, 3)
    s = append(s, 4)
    s[1] = 111
    fmt.Printf("func s: %v, addr: %p \n", s, s)
}

运行输出:

slice: [0 1], addr: 0xc0000a0140 
func s: [0 111 3 4], addr: 0xc0000ca030 
slice: [0 1], addr: 0xc0000a0140

把changeSlice的s[1] = 111操作提前:

func main() {
    slice := make([]int, 2, 3)
    for i := 0; i < len(slice); i++ {
        slice[i] = i
    }
    fmt.Printf("slice: %v, addr: %p \n", slice, slice)
    changeSlice(slice)
    fmt.Printf("slice: %v, addr: %p \n", slice, slice)
}
func changeSlice(s []int){
  s[1] = 111
    s = append(s, 3)
    s = append(s, 4)
    fmt.Printf("func s: %v, addr: %p \n", s, s)
}

运行输出:

slice: [0 1], addr: 0xc0000a0140 
func s: [0 111 3 4], addr: 0xc0000ca030 
slice: [0 111], addr: 0xc0000a0140 

首先,从实参和形参的地址可以看出来,实参和形参是两个切片,传参过程中是复制关系,这个不重要,指针传递时也是这样。
第二,实参和形参指向同一个数组,这个也不重要,指针传递时形参和实参也是指向同一片内存区域。
但是上面的代码,先扩容,形参slice的底层数组更换了(相当于形参指针指向了新的内存区域,即给指针重新赋值,但是并没有显式进行这个操作,不深入了解切片可能看不出来),所以s[1] = 111不会影响到实参slice的底层数组,修改也就不会体现在实参slice中。下面的代码,先修改,修改发生在形参slice的底层数组上,也是实参slice的底层数组。所以修改体现在实参slice中。
Append如果发生扩容,相当于修改了指针指向的内存区域。




更新的分割线,下面是以前的理解,是错误的


  Go数组是值类型,赋值给其他数组和函数传参操作都会复制整个数组数据,如果数组特别大,传来传去浪费大量内存。所以Go搞出了一个切片类型,切片类型的变量是指向一个数组的指针。对切片的操作,就是操作底层的数组。
  这倒是和java很象,java的数组不是基本类型,是引用类型。
  我们知道切片初始化有3种方式。
第一种:

arr := [2]int{100, 200} // 指定了大小的就是数组
sli := []int{100, 200}  // 没指定大小的就是切片

第二种:

sli := make(int64, 5, 10) // 第三个参数容量可以省略,等于第二个参数长度。底层数组大小等于切片容量

第三种:

arr := [5]int{1,2,3,4,5}
sli := arr[1:2] // 初始化切片sli,是数组arr的引用

  切片有length和capacity两个概念,length小于等于capacity。capacity不够用了就扩容,扩容的本质是更换一个新的底层数组,反之,不扩容只增加length不会更换底层数组。
  在第三种方式中,sli := arr[1:2]其实隐含了sli := arr[1:2:5]的意思,这里1、2、5都是数组下标,slice[ i : j : k]的length就是j - i,capacity就是k - i,sli的底层数组就是arr,这时候对sli的修改就是对arr的修改。
  这里也不绝对,通过append增加sli的length,直到达到capacity触发扩容,sli就会更换一个新的底层数组(arr不够用了),此后对于sli的修改就不会改变arr了。
所以,一般建议使用

sli := arr[1:2:2]

  只要append就会切换到新的底层数组,不影响原来的数组。当然你如果没有append直接改变现有的值,还是会改变数组的值。
  时刻记住切片是指向数组的指针。

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