首页 > 其他分享 >Golang 切片作为函数参数传递的陷阱与解答

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

时间:2024-07-09 20:29:57浏览次数:13  
标签:参数传递 slice len Golang 数组 main mSlice 切片

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

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)

标签:参数传递,slice,len,Golang,数组,main,mSlice,切片
From: https://www.cnblogs.com/linguanh/p/18292709

相关文章

  • golang-数组基本使用
    Go语言中的数组是一个固定长度的元素序列,这些元素都是相同的类型。数组是值类型,这意味着当它们被赋值给新的变量时,会进行一次数组的复制。创建数组通过指定元素类型和数组长度来创建数组。数组长度必须是一个常量表达式,因为数组的长度是其类型的一部分。数组的数量一旦确定就不......
  • go 切片初始化
    当在Go语言中使用切片时,切片的初始化是一个常见的操作。在本篇博客中,我们将介绍切片的初始化方式以及一些常见的示例。什么是切片初始化?切片是一个动态数组,可以根据需要自动调整大小。切片的初始化是为切片分配底层数组并设置切片的长度和容量。切片可以通过字面量、make函数......
  • 03.切片的概念
    切片(Slice)是Go语言中非常重要的一种数据结构,用于处理具有动态长度的序列。切片是对数组的抽象,可以更灵活地操作和管理数组的数据。下面详细介绍切片的概念及其核心特点。切片的基本概念定义切片是一个引用类型,它指向一个底层数组的一部分或全部。与数组不同,切片的长度可以动......
  • 使用mybatis切片实现数据权限控制
     数据权限控制需要对查询出的数据进行筛选,对业务入侵最少的方式就是利用mybatis或者数据库连接池的切片对已有业务的sql进行修改。切片逻辑完成后,仅需要在业务中加入少量标记代码,就可以实现对数据权限的控制。这种修改方式,对老业务的逻辑没有入侵或只有少量入侵,基本不影响老业......
  • 02.Slice 切片
    切片(Slice)是Go语言中比数组更为灵活和强大的数据结构。切片本质上是对数组的一个视图,可以动态调整大小。下面详细介绍切片的定义、初始化、操作和使用注意事项。切片的定义与初始化切片的定义语法为:[]T,其中T表示切片元素的类型。切片的初始化可以通过数组字面量、内置函数m......
  • Go语言--复合类型之切片
    slice概念数组的长度在定义之后无法再次修改;数组是值类型,每次传递都将产生一份副本。显然这种数据结构无法完全满足开发者的真实需求。Go语言提供了数组切片(slice)来弥补数组的不足。切片并不是数组或数组指针,它通过内部指针和相关属性引用数组片段,以实现变长方案。sli......
  • 001,函数指针是一种特殊的指针,它指向的是一个函数地址,可以存储函数并作为参数传递,也可
    函数指针是一种特殊的指针001,函数指针是一种特殊的指针,它指向的是一个函数地址,可以存储函数并作为参数传递,也可以用于动态绑定和回调函数文章目录函数指针是一种特殊的指针前言总结前言#include<iostream>usingnamespacestd;intadd(inta,intb){ return......
  • 人工智能在病理切片虚拟染色及染色标准化领域的系统进展分析|文献速递·24-07-07
    小罗碎碎念本期文献主题:人工智能在病理切片虚拟染色及染色标准化领域的系统进展分析这一期文献的速递,是有史以来数量最大的一次,足足有十一篇,本来打算分两期写,但是为了知识的系统性,我决定咬咬牙,放在同一期推文里。关于病理切片虚拟染色和染色标准化的研究,之前写过一期推......
  • Golang channel底层是如何实现的?(深度好文)
    Hi你好,我是k哥。大厂搬砖6年的后端程序员。我们知道,Go语言为了方便使用者,提供了简单、安全的协程数据同步和通信机制,channel。那我们知道channel底层是如何实现的吗?今天k哥就来聊聊channel的底层实现原理。同时,为了验证我们是否掌握了channel的实现原理,本文也收集了channel的高......
  • Golang: 解引用 赋值 时发生了什么
    Golang:解引用赋值时发生了什么示例代码packagemainimport"fmt"typeComplexStructstruct{ Aint Bstring Cfloat64 Dbool E[]int Fmap[string]string G*ComplexStruct}funcmain(){ com1:=ComplexStruct{ A:1, B:"com1", C:3......