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
Jirajine 2021-07-15 21:10:46 +08:00 via Android
slice 是一个 fat pointer (即一个 pointer 加一个 length ),而函数都是值传递,append 会自动扩容(容量不够时分配一个新数组并把数据复制过去),返回指向(无论新旧)数组的 slice 。
传统 OO 语言一切皆对象(引用)一致性更好,也没有这些心智负担,go 这种保留裸指针纯粹是开 dao 车。 |
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 底下 |
3
labulaka521 2021-07-15 23:20:11 +08:00
|
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] |
5
iyear OP |
6
BeautifulSoap 2021-07-15 23:54:56 +08:00 via Android
一开始没反应过来,然后才想到,把 slice 看成含有指向目标内存块指针,len,cap 三个元素的 struct 值就好理解发生的这一切了。就是上面官方文档里的那样
至于为什么超了 cap 要重新分配内存,lz 你学 go 的时候应该知道 slice 底层是数组,而数组在内存上是连续空间,你想要获得更大的连续空间的话,只能重新申请一块新的连续空间(无视长度直接继续写就是内存泄漏了,鬼知道会发生什么,学 C 的人一定很亲切) 至于有没有办法用不连续内存存数据?当然了,链表之类的就行 |
7
iyear OP @BeautifulSoap 嗯这就是我的意思,我写的是针对 append 下带坑的两种情况的解释
|
8
iyear OP slice 的实现是已经了解过。想询问的是这两种情况下都会导致数据问题的理解有无问题,还是有其他原因
|
9
hannibalm 2021-07-16 16:42:16 +08:00
把 slice 当作一种数据类型,给函数传值,则在该函数只读 slice ;给函数传 slice 地址,则可读可修改。
以前 C 语言就是这个思路。 |
10
EscYezi 2021-07-18 05:00:48 +08:00 via iPhone
我记得 golang 圣经里面有提过参数传递 slice,函数内操作不会导致 slice 本身发生变化,但是可以使 slice 指向的底层数组变化
|
11
EscYezi 2021-07-18 05:05:02 +08:00 via iPhone
类似于传递指针,对这个指针操作不会改变指针本身( slice 的 len 和 cap 属性),而是指针指向的对象(底层数组)
|
12
qwertyzzz 2021-07-18 12:24:33 +08:00
之前刷题的时候遇到过 都是复制一份出来操作的 也不太明白
|
13
barathrum 2021-07-19 11:48:33 +08:00
@BeautifulSoap 应该叫溢出不叫泄露吧。
|
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. |
15
iyear OP @zhangsanfeng2012 #14 感谢原来官方文档已经有说明了
|