• Golang 切片作为函数参数传递的陷阱与解答


    作者:林冠宏 / 指尖下的幽灵。转载者,请: 务必标明出处。

    GitHub : https://github.com/af913337456/

    出版的书籍:


    • 例子
      • 切片作为函数参数传递的是值
      • 用来误导切片作为函数参数传递的是引用
      • 函数内切片 append 引起扩容的修改将无效
      • 不引起切片底层数组扩容,验证没指向新数组
      • 脚踏实地让切片在函数内的修改
    • 彩蛋

    切片 slice 几乎是每个 Go 开发者以及项目中 100% 会高频使用到的,Go 语言的知识很广,唯独 slice 我个人认为是必须要深入了解的。

    乃至于今,网上还有很多关于切片 slice 技术文章一直存在的错误内容:切片作为函数参数传递的是引用,这是错误的。

    无论是官方说明还是实践操作都表明:切片作为函数参数传递的是值,和数组一样。


    接下来我们直接看例子以加深印象。

    切片作为函数参数传递的是值的例子:
    func main() {
        mSlice := []int{1, 2, 3}
        fmt.Printf("main-1: %p \n", &mSlice) // 0x140000b2000
        mAppend(mSlice)
        fmt.Printf("main-2: %p \n", &mSlice) // 0x140000b2000
    }
    
    func mAppend(slice []int) {
        fmt.Printf("append func: %p \n", &slice) // 0x140000b2018 和外部的不一样
    }
    
    错觉例子,也是现在用来误导切片作为函数参数传递的是引用的错误文章常用的
    func main() {
        mSlice := []int{1, 2, 3}
        fmt.Printf("main-1: %v \n", mSlice) // [1,2,3]
        mAppend(mSlice)
        fmt.Printf("main-2: %v \n", mSlice) // [1,9,3],这里2被修改了,但不是引用传递导致的
    }
    
    func mAppend(slice []int) {
        slice[2] = 9 // 修改
    }
    
    切片的内部结构:
    // 源码路径:go/src/runtime/slice.go
    type slice struct {
        array unsafe.Pointer // 指针
        len   int
        cap   int
    }
    

    切片的本质是 struct,作为函数参数传递时候遵循 struct 性质,array 是指针指向一个数组,len 是数组的元素个数,cap 是数组的的长度。当 len > cap,将触发数组扩容。

    解析: 为什么上面的 错觉例子 能在函数内部改变值且在外部生效。这是因为当切片作为参数传递到函数里,虽然是值传递,但函数内拷贝出的新切片的 array 指针所指向的数组和外部的旧切片是一样的,那么在没引起扩容情况下进行值的修改就生效了。

    旧切片 array 指针 ---> 数组-1

    新切片 array 指针 ---> 数组-1,函数内发生改变


    函数内切片 append 引起扩容的修改将无效的例子:
    func main() {
        mSlice := []int{1, 2, 3}
        fmt.Printf("main-1: %v \n", mSlice) // [1,2,3]
        mAppend(mSlice)
        fmt.Printf("main-2: %v \n", mSlice) // [1,2,3] 没生效
    }
    
    func mAppend(slice []int) {
        // slice[2] = 9 // 修改
        slice = append(slice, 4)
        fmt.Printf("append: %v \n", slice) // [1,2,3,4]
    }
    

    解析:切片初始化时候添加了3个数,导致其 len 和 cap 都是3,函数内添加第四个数的时候,触发扩容,而扩容会导致扩容,array 指针指向新的数组,在函数结束后,旧切片数组并没修改。

    旧切片 array 指针 ---> 数组-1 值 [1,2,3]

    新切片 array 指针 ---> 数组-2 值 [1,2,3,4]


    不引起切片底层数组扩容,验证没指向新数组例子:
    func main() {
        mSlice := make([]int, 3, 4) // len = 3, cap = 4, cap > len
        fmt.Printf("main-1: %v, 数组地址: %p \n", mSlice, mSlice) // [0,0,0], 0x14000120000
        mAppend(mSlice)
        fmt.Printf("main-2: %v, 数组地址: %p \n", mSlice, mSlice) // [0,0,0], 0x14000120000 
    }
    
    func mAppend(slice []int) {
        slice = append(slice, 4)
        fmt.Printf("append: %v, 数组地址: %p \n", slice, slice) // [0,0,0,4], 0x14000120000
    }
    

    解析:可以看到切片的底层数组地址并没改变,但是数组的值依然没改变。这是因为切片是值传递到函数内部的,此时的 len 依然是值传递,当打印的时候,就只打印 len 以内的数据。

    旧切片 len = 3

    新切片 len = 4,函数内改变


    至此,我们应该如何让切片在函数内的修改生效?答案就是规规矩矩使用指针传参

    func main() {
        mSlice := []int{1, 2, 3}
        fmt.Printf("main-1: %v, 数组地址: %p \n", mSlice, mSlice) // [1,2,3], 0x1400001a0a8
        mAppend(&mSlice)
        fmt.Printf("main-2: %v, 数组地址: %p \n", mSlice, mSlice) // [1,2,3,4], 0x1400001a0a8
    }
    
    func mAppend(slice *[]int) {
        *slice = append(*slice, 4)
        fmt.Printf("append: %v, 数组地址: %p \n", *slice, slice) // [1,2,3,4], 0x140000181b0
    }
    

    上面例子成功在函数内使用 append 修改了切片,也可以看到切片数组地址变了,这是因为引起了扩容。但 array 指针没变,所以扩容后,指向了新的。

    切片 array 指针 ---> 数组-1 值 [1,2,3]

    切片 array 指针 ---> 数组-2 值 [1,2,3,4]

    彩蛋

    切片的扩容:

    • go1.18之前,临界值为1024,len 小于1024时,切片先2倍 len 扩容。大于 1024,每次增加 25% 的容量,直到新容量大于期望容量;

    • go1.18之后,临界值为256,len 小于256,依然2倍 len 扩容。大于256走算法:newcap += (newcap + 3*threshold) / 4,直到满足。(threshold = 256)

  • 相关阅读:
    OpenCV图像特征提取学习二,Shi-Tomasi 角点检测算法
    【仿牛客网笔记】Spring Boot实践,开发社区登录模块-显示登录信息
    自助商业智能平台 HK-Visokio Omniscope 详解!
    Open3D (C++) 计算两点云之间的最小距离
    【C/C++】递归算法
    vue.use()所做的事情
    python requests请求接口返回304问题解决
    搜索框的测试
    『C++ - STL』之优先级队列( priority_queue )
    从0开始python学习-54.python中flask创建MD5和base64加密校验的接口
  • 原文地址:https://www.cnblogs.com/linguanh/p/18292709