slice 在 append 不同情况下的理解

2021-07-15 20:54:28 +08:00
 iyear

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 的内部扩容实现导致?

这样理解有没有问题?

2051 次点击
所在节点    Go 编程语言
15 条回复
Jirajine
2021-07-15 21:10:46 +08:00
slice 是一个 fat pointer (即一个 pointer 加一个 length ),而函数都是值传递,append 会自动扩容(容量不够时分配一个新数组并把数据复制过去),返回指向(无论新旧)数组的 slice 。
传统 OO 语言一切皆对象(引用)一致性更好,也没有这些心智负担,go 这种保留裸指针纯粹是开 dao 车。
CEBBCAT
2021-07-15 22:37:47 +08:00
你第二个测试根本不完备嘛,既没有验证修改之后对原 slice 有没有影响,也没有验证 slice 的 cap

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

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

或者去看 go 源码,记得是在 runtime 底下
labulaka521
2021-07-15 23:20:11 +08:00
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
2021-07-15 23:51:03 +08:00
@ongongethan 这我知道,只是针对这个问题
@CEBBCAT 还是官方博客权威啊
BeautifulSoap
2021-07-15 23:54:56 +08:00
一开始没反应过来,然后才想到,把 slice 看成含有指向目标内存块指针,len,cap 三个元素的 struct 值就好理解发生的这一切了。就是上面官方文档里的那样

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

至于有没有办法用不连续内存存数据?当然了,链表之类的就行
iyear
2021-07-15 23:59:01 +08:00
@BeautifulSoap 嗯这就是我的意思,我写的是针对 append 下带坑的两种情况的解释
iyear
2021-07-16 00:11:02 +08:00
slice 的实现是已经了解过。想询问的是这两种情况下都会导致数据问题的理解有无问题,还是有其他原因
hannibalm
2021-07-16 16:42:16 +08:00
把 slice 当作一种数据类型,给函数传值,则在该函数只读 slice ;给函数传 slice 地址,则可读可修改。
以前 C 语言就是这个思路。
EscYezi
2021-07-18 05:00:48 +08:00
我记得 golang 圣经里面有提过参数传递 slice,函数内操作不会导致 slice 本身发生变化,但是可以使 slice 指向的底层数组变化
EscYezi
2021-07-18 05:05:02 +08:00
类似于传递指针,对这个指针操作不会改变指针本身( slice 的 len 和 cap 属性),而是指针指向的对象(底层数组)
qwertyzzz
2021-07-18 12:24:33 +08:00
之前刷题的时候遇到过 都是复制一份出来操作的 也不太明白
barathrum
2021-07-19 11:48:33 +08:00
@BeautifulSoap 应该叫溢出不叫泄露吧。
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
2021-07-21 09:44:24 +08:00
@zhangsanfeng2012 #14 感谢原来官方文档已经有说明了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/789788

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX