golang append函数破坏原有slice数据

CodingCode · · 484 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

append函数破坏原有slice数据

先从slice添加元素开始

假设往整数slice指定位置添加元素,例如:

func main() {
    s0 := []string{"a", "b", "c", "d", "e"}

    i := 2
    s9 := append(append(s0[:i], "X"), s0[i:]...)

    print("s0", s0)
    print("s9", s9)
}

原意把“X”插入到数组s0的位置2,也就是“c“的前面;这里采用的办法是两次append。

  1. 第一个取出子slice ["a", "b"]
  2. 往子slice加入X,append(["a", "b"], "X") ==> ["a", "b", "X"]
  3. 在加入后半部分, append(["a", "b", "X"], ["c", "d", "e"]) ==> ["a", "b", "X", "c", "d", "e"]

运行结果:

s0: ["a", "b", "X", "d", "e"]
s9: ["a", "b", "X", "X", "d", "e"]

和预想的完全不一样,不但s9不对,而且原slice s0也发生了改变,这究竟发生了什么???

我们把上述步骤分解开来看:

func main() {
    s0 := []string{"a", "b", "c", "d", "e"} ; print("s0", &s0)

    i := 2
    //s9 := append(append(s0[:i], "X"), s0[2:]...)
    s1 := s0[:i]            ; print("s1", &s1)
    s2 := append(s1, "X")   ; print("s2", &s2)
    s3 := s0[i:]            ; print("s3", &s3)
    s9 := append(s2, s3...) ; print("s9", &s9)
}

运行结果:

s0: [a b c d e]
s1: [a b]
s2: [a b X]
s3: [X d e]
s9: [a b X X d e]

发现s3不对了,而s3 := s0[i:],这之前只有s2 := append(s1, "X")一条语句,难道这条语句改变了s3(s0)的内容啦?我们挪动一下s2和s3的位置两者交换,让s3先运行,结果是s3是:

s0: [a b c d e]
s1: [a b]
s3: [c d e]
s2: [a b X]
s9: [a b X X d e]

可见就是append函数破坏了原来s0的内容。

那么append为什么会破坏呢?我们增强一下代码把slice的内容打出来:

package main

import (
    "fmt"
    "unsafe"
)

func printslice(n string, s *[]string) {
    type SliceStruct struct {
        ptr uintptr
        len uint64
        cap uint64
    }
    p := (* SliceStruct)(unsafe.Pointer(s));

    fmt.Printf("%s: p=%p,ptr=0x%x,len=%d,cap=%d,val=%v\n", n, p, p.ptr, p.len, p.cap, s)
}


func main() {
    s0 := []string{"a", "b", "c", "d", "e"} ; printslice("s0", &s0)
    
    i := 2
    //s9 := append(append(s0[:i], "X"), s0[2:]...)
    s1 := s0[:i]            ; printslice("s1", &s1)
    s2 := append(s1, "X")   ; printslice("s2", &s2)
    s3 := s0[i:]            ; printslice("s3", &s3)
    s9 := append(s2, s3...) ; printslice("s9", &s9)
}

运行结果:

s0: p=0xc0000a0020,ptr=0xc0000b6000,len=5,cap=5,val=&[a b c d e]
s1: p=0xc0000a0060,ptr=0xc0000b6000,len=2,cap=5,val=&[a b]
s2: p=0xc0000a00a0,ptr=0xc0000b6000,len=3,cap=5,val=&[a b X]
s3: p=0xc0000a00e0,ptr=0xc0000b6020,len=3,cap=3,val=&[X d e]
s9: p=0xc0000a0120,ptr=0xc0000c2000,len=6,cap=10,val=&[a b X X d e]

这里我们看到问题了没有,s0, s1, s2他们的数据指针值是相同的,也就是说slice s0, s1, s2指向的是同一块数据内存,然而数据大小(len)不一致,所以问题的根源在于:
语句s2 := append(s1, "X")有很大的问题,s1和s0共用数据空间,然后s1的len是2,于是append(s1, ...)的时候把新数据插入到s1的下标为s1[2]的位置,而这个位置原来就是s0[2]的位置,于是就把s0[i]的原来的值("c")给覆盖掉了。
其根源在于s1并没有独立分配出来自己的数据空间,还是使用的s0的数据空间。

有没有办法解决这个问题呢?又要用到append的一个属性。
如果append后的slice的长度大于capacity值,那么append就会申请新的数据内容空间,这样就不会破坏原有的s0的数据空间。
前面我们看到s1的len是2,而cap的值是5,这是从s0拷贝过来的,于是append(s1, "X")的时候,append计算2+1 < 5就不分配新的空间,就直接在s1[2]的位置替换“X”,就冲掉了s0的"c"值,那么如果我们把s1的cap值改成2,那么2+1>2,会导致append重启分配新的空间,我们试一下,这是一个临时hack的办法:

func printslice(n string, s *[]string) {
    type SliceStruct struct {
        ptr uintptr
        len uint64
        cap uint64
    }
    p := (* SliceStruct)(unsafe.Pointer(s));

    if n == "s1" {
        p.cap = 2   // rewrite cap to 2 for s1 slice by force
    }
    fmt.Printf("%s: p=%p,ptr=0x%x,len=%d,cap=%d,val=%v\n", n, p, p.ptr, p.len, p.cap, s)
}

再运行:

s0: p=0xc0000bc000,ptr=0xc0000be000,len=5,cap=5,val=&[a b c d e]
s1: p=0xc0000bc040,ptr=0xc0000be000,len=2,cap=2,val=&[a b]
s2: p=0xc0000bc080,ptr=0xc0000c0040,len=3,cap=4,val=&[a b X]
s3: p=0xc0000bc0c0,ptr=0xc0000be020,len=3,cap=3,val=&[c d e]
s9: p=0xc0000bc100,ptr=0xc0000cc000,len=6,cap=8,val=&[a b X c d e]

看到s2的数据地址就和s0,s1不一样,是重新另分配出来的,就不会导致数据被重写,而且我们看到最终的结果s9也是所期望的值。

总结就是我们要理解append函数的定义,看文档,不能按我们自己的理解来使用append。

最后一个问题,如何更改slice的cap值呢,好像不行唉。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:CodingCode

查看原文:golang append函数破坏原有slice数据

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

484 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传