V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
iyear
V2EX  ›  Go 编程语言

slice 在 append 不同情况下的理解

  •  
  •   iyear ·
    iyear · 2021-07-15 20:54:28 +08:00 · 2048 次点击
    这是一个创建于 1227 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Q 群里讨论起来 slice 的传递,才发现有坑 = =

    看了网上的一些文章。slice 在传入函数后 append 会有坑 下面是我的理解,不知道恰不恰当。

    一种情况是 cap 够,不扩容

    func main() {
        sliceA := make([]int, 3, 4)
        sliceA[0] = 0
        sliceA[1] = 1
        sliceA[2] = 2
        fmt.Println(sliceA)
        changeSlice(sliceA)
        fmt.Println(sliceA)
        fmt.Println(sliceA[:4])
    }
    func changeSlice(slicePass []int) {
        slicePass = append(slicePass, 3)
    }
    
    //Output
    /*
    [0 1 2]
    [0 1 2]
    [0 1 2 3]
    */
    

    slice 结构中的 len,cap 都是 int,无法在函数里面被改变。 这种情况下指定了一个 len 为 3,cap 为 4 的 slice 。append 完后发现正常输出只会输出前三个数,验证了 len 并没有被改变。而当强制输出第四项时又发现 3 是存在的。 例如截取数组等操作都是这种情况。

    也就是说这种情况下 append 对原数组生效,只是由于 len 没有改变而无法呈现出 append 的项。

    还有种情况是 cap 不够,slice 扩容

    slice 扩容会把扩容后的数组指向新内存,直接与原数组无关了,append 的项也不存在于原数组 大概代码长这样

    func main() {
        sliceA := []int{1, 2, 3, 4, 5}
        fmt.Println(sliceA)
        fmt.Printf("%d %p main\n", len(sliceA),sliceA)
        changeSliceA(sliceA)
        fmt.Println(sliceA)
    }
    func changeSliceA(slicePass []int) {
        slicePass = append(slicePass, 6)
        fmt.Printf("%d %p pass\n", len(slicePass),slicePass)
    }
    
    // Output
    /*
    [1 2 3 4 5]
    5 0xc00000c690 main
    6 0xc000016550 pass
    [1 2 3 4 5]
    */
    
    

    所以是 go 中的 slice 在函数中被 append 时数据呈现不变分为两种情况。

    一种是 len 未被改变,由传值导致;

    一种是指针发生改变,由 slice 的内部扩容实现导致?

    这样理解有没有问题?

    第 1 条附言  ·  2021-07-17 01:48:17 +08:00
    可能是我描述的问题,感觉大家没看懂我想问的是什么。我是想问问大家我这样两种情况的理解有没有错,大家都在给我科普 slice 的原理😂
    然后把这个扩写成了一篇小短文,好久没写博客了,语言组织能力急剧下降。有空再完善一下代码
    https://blog.ljyngup.com/archives/868.html/
    15 条回复    2021-07-21 09:44:24 +08:00
    Jirajine
        1
    Jirajine  
       2021-07-15 21:10:46 +08:00 via Android
    slice 是一个 fat pointer (即一个 pointer 加一个 length ),而函数都是值传递,append 会自动扩容(容量不够时分配一个新数组并把数据复制过去),返回指向(无论新旧)数组的 slice 。
    传统 OO 语言一切皆对象(引用)一致性更好,也没有这些心智负担,go 这种保留裸指针纯粹是开 dao 车。
    CEBBCAT
        2
    CEBBCAT  
       2021-07-15 22:37:47 +08:00 via Android   ❤️ 1
    你第二个测试根本不完备嘛,既没有验证修改之后对原 slice 有没有影响,也没有验证 slice 的 cap

    不要听楼上瞎扯,Python 、Java 也有深浅复制问题。

    关于 slice,强烈推荐去读 Golang 的博客
    附链接: https://blog.golang.org/slices-intro

    或者去看 go 源码,记得是在 runtime 底下
    ongongethan
        4
    ongongethan  
       2021-07-15 23:29:41 +08:00
    使用切片指针,可以解决上面两个例子的问题。

    例子 1
    func main() {
    sliceA := make([]int, 3, 4)
    sliceA[0] = 0
    sliceA[1] = 1
    sliceA[2] = 2
    fmt.Println(sliceA)
    changeSlice(&sliceA)
    fmt.Println(sliceA)
    fmt.Println(sliceA[:4])
    }
    func changeSlice(slicePass *[]int) {
    *slicePass = append(*slicePass, 3)
    }

    输出:
    [0 1 2]
    [0 1 2 3]
    [0 1 2 3]


    例子 2
    func main() {
    sliceA := []int{1, 2, 3, 4, 5}
    fmt.Printf("main before append: len:%d address:%p\n", len(sliceA),sliceA)
    fmt.Println("content:", sliceA)
    changeSliceA(&sliceA)
    fmt.Printf("main after append: len:%d address:%p\n", len(sliceA),sliceA)
    fmt.Println("content:", sliceA)
    }

    func changeSliceA(slicePass *[]int) {
    fmt.Printf("func before append: len:%d address:%p\n", len(*slicePass),*slicePass)
    fmt.Println("content:", *slicePass)
    *slicePass = append(*slicePass, 6)
    fmt.Printf("func after append: len:%d address:%p\n", len(*slicePass),*slicePass)
    fmt.Println("content:", *slicePass)
    }

    输出:
    main before append: len:5 address:0xc00007a030
    content: [1 2 3 4 5]
    func before append: len:5 address:0xc00007a030
    content: [1 2 3 4 5]
    func after append: len:6 address:0xc000014050
    content: [1 2 3 4 5 6]
    main after append: len:6 address:0xc000014050
    content: [1 2 3 4 5 6]
    iyear
        5
    iyear  
    OP
       2021-07-15 23:51:03 +08:00 via Android
    @ongongethan 这我知道,只是针对这个问题
    @CEBBCAT 还是官方博客权威啊
    BeautifulSoap
        6
    BeautifulSoap  
       2021-07-15 23:54:56 +08:00 via Android
    一开始没反应过来,然后才想到,把 slice 看成含有指向目标内存块指针,len,cap 三个元素的 struct 值就好理解发生的这一切了。就是上面官方文档里的那样

    至于为什么超了 cap 要重新分配内存,lz 你学 go 的时候应该知道 slice 底层是数组,而数组在内存上是连续空间,你想要获得更大的连续空间的话,只能重新申请一块新的连续空间(无视长度直接继续写就是内存泄漏了,鬼知道会发生什么,学 C 的人一定很亲切)

    至于有没有办法用不连续内存存数据?当然了,链表之类的就行
    iyear
        7
    iyear  
    OP
       2021-07-15 23:59:01 +08:00 via Android
    @BeautifulSoap 嗯这就是我的意思,我写的是针对 append 下带坑的两种情况的解释
    iyear
        8
    iyear  
    OP
       2021-07-16 00:11:02 +08:00 via Android
    slice 的实现是已经了解过。想询问的是这两种情况下都会导致数据问题的理解有无问题,还是有其他原因
    hannibalm
        9
    hannibalm  
       2021-07-16 16:42:16 +08:00
    把 slice 当作一种数据类型,给函数传值,则在该函数只读 slice ;给函数传 slice 地址,则可读可修改。
    以前 C 语言就是这个思路。
    EscYezi
        10
    EscYezi  
       2021-07-18 05:00:48 +08:00 via iPhone
    我记得 golang 圣经里面有提过参数传递 slice,函数内操作不会导致 slice 本身发生变化,但是可以使 slice 指向的底层数组变化
    EscYezi
        11
    EscYezi  
       2021-07-18 05:05:02 +08:00 via iPhone
    类似于传递指针,对这个指针操作不会改变指针本身( slice 的 len 和 cap 属性),而是指针指向的对象(底层数组)
    qwertyzzz
        12
    qwertyzzz  
       2021-07-18 12:24:33 +08:00
    之前刷题的时候遇到过 都是复制一份出来操作的 也不太明白
    barathrum
        13
    barathrum  
       2021-07-19 11:48:33 +08:00
    @BeautifulSoap 应该叫溢出不叫泄露吧。
    zhangsanfeng2012
        14
    zhangsanfeng2012  
       2021-07-20 10:41:56 +08:00
    https://golang.org/doc/effective_go 官方文档两句话
    1 、If a function takes a slice argument, changes it makes to the elements of the slice will be visible to the caller, analogous to passing a pointer to the underlying array.
    2 、We must return the slice afterwards because, although Append can modify the elements of slice, the slice itself (the run-time data structure holding the pointer, length, and capacity) is passed by value.
    iyear
        15
    iyear  
    OP
       2021-07-21 09:44:24 +08:00
    @zhangsanfeng2012 #14 感谢原来官方文档已经有说明了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2539 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 15:28 · PVG 23:28 · LAX 07:28 · JFK 10:28
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.