Golang传参slice函数中append的陷阱
今天我写了类似这样一段bug:
func AppendIfNotExist(hobbies []string, target string) bool {
for _, v := range hobbies {
if v == target {
return true
}
}
hobbies = append(hobbies, target)
return false
}
func main() {
hobbies := []string{"sing", "dance"}
exist := AppendIfNotExist(hobbies, "rap")
if !exist {
fmt.Println(hobbies)
}
}
因为slice类型作为参数传递是可以影响到实参的,所以我信心满满的期待返回[sing dance rap]
,然而事实证明rap并没有被append到原slice。
无奈之下看了一眼源码,才发现了问题所在,slice的定义如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice本质上就是就是一个结构体,包括一个指向底层数据的指针以及len与cap,而Go语言只有值传递,所以传入的实际上是结构体的拷贝
func twice(x []int) {
for i := range x {
x[i] *= 2
}
}
// 模拟slice参数传递
type IntSliceHeader struct {
Data []int
Len int
Cap int
}
func twice(x IntSliceHeader) {
for i := 0; i < x.Len; i++ {
x.Data[i] *= 2
}
}
当进行append操作时,在底层数据后面追加元素同时len+1,在源码中可以看到当容量不够时的扩容方式如下:
func growslice(et *_type, old slice, cap int) slice {
// ...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
// ...
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
// Only clear the part that will not be overwritten.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// Only shade the pointers in old.array since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
}
}
memmove(p, old.array, lenmem)
}
当长度小于1024字节时,会扩容为原来的二倍,超过1024时每次增长原来的1/4,最后调用mallocgc重新分配一块内存,整个过程如下:
当修改原slice中的元素时,由于指针指向的内存空间相同,所以能够对实参生效,而append操作就不能保证了
即使没有发生扩容而导致的重新分配内存,由于实参的len并没有增大,所以也是访问不了的,不过这时可以用unsafe中的Pointer和Sizeof进行越界访问:
hobbies := []string{"sing"}
exist := AppendIfNotExist(hobbies, "dance")
pointer := unsafe.Pointer(uintptr(unsafe.Pointer(&hobbies[0])) + 2*unsafe.Sizeof(hobbies[0]))
fmt.Println(*((*string)(pointer)))
// Output:
// dance
总结
其实如果认真看完The Go Programming Language,应该能立刻反应到问题出在哪里,无奈我只记住了slice可以影响到实参,有空还是应该再仔细啃一遍。