cmatrixprobe

发呆业务爱好者

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可以影响到实参,有空还是应该再仔细啃一遍。

移动SSD安装配置WTG(Windows To Go)

上一篇

从零开始Golang爬虫(一)| 预备知识

下一篇
评论
头像 发表评论 说点什么
还没有评论
1186
0