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

golang · 浏览次数 : 28

小编点评

本文主要讨论了 Go 语言中切片(slice)作为函数参数传递的问题,纠正了一些常见的误解,并通过实例分析了切片作为值传递时的行为。 1. **切片作为函数参数传递的是值**:文章首先指出,切片作为函数参数传递的是值,而非引用。通过示例代码,展示了切片作为值传递时,函数内部对切片的修改不会影响外部变量。 2. **常见的误解与错误内容**:文章接着分析了网络上关于切片作为函数参数传递的常见误解,例如认为切片作为函数参数传递的是引用。通过实例代码,揭示了这种误解的错误性。 3. **切片的内部结构**:文章解释了切片的内部结构,包括其字段 array、len 和 cap,以及它们如何影响切片的行为。 4. **切片作为函数参数时的修改无效**:文章通过示例代码说明了,即使在函数内部对切片进行了修改,这些修改也不会在外部产生影响,因为切片作为值传递,函数内部不会改变原始切片的引用。 5. **扩容机制**:文章还讨论了切片的扩容机制,包括临界值的变化和扩容策略。 6. **如何让切片在函数内的修改生效**:最后,文章通过示例代码展示了如何通过指针传递切片来使函数内的修改生效。 总的来说,文章强调了正确理解切片在 Go 语言中的行为对于编写可靠代码的重要性,并通过实例分析和解释,帮助读者更好地掌握切片的使用。

正文

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

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)

与Golang 切片作为函数参数传递的陷阱与解答相似的内容:

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

作者:林冠宏 / 指尖下的幽灵。转载者,请: 务必标明出处。 GitHub : https://github.com/af913337456/ 出版的书籍: 《1.0-区块链DApp开发实战》 《2.0-区块链DApp开发:基于公链》 例子 切片作为函数参数传递的是值 用来误导切片作为函数参数传递的

golang 所有关键字的列表及释义归类

golang 所有关键字的列表及释义归类,截至1.18版本。 [控制结构] if : 条件语句,基于布尔表达式的值决定是否执行特定的代码块。 else、 else if : 用在 if 语句之后,当条件表达式为假时执行的代码块。 switch : 多路选择语句,根据不同的情况执行不同的代码块。 ca

golang 泛型的格式写法

Go语言中的泛型(Generics)是在 Go 1.18 版本中引入的一个重要特性,它允许你编写可重用的代码,而不需要为每种数据类型重复编写相同的逻辑。 泛型通过参数化类型(type parameters)来实现,使得函数、方法、接口和结构体可以与多种类型一起工作。 下面详细介绍Go语言中泛型的基本

Golang channel底层是如何实现的?(深度好文)

Go语言为了方便使用者,提供了简单、安全的协程数据同步和通信机制,channel。那我们知道channel底层是如何实现的吗?今天k哥就来聊聊channel的底层实现原理。同时,为了验证我们是否掌握了channel的实现原理,本文也收集了channel的高频面试题,理解了原理,面试题自然不在话下。

Golang 依赖注入设计哲学|12.6K 的依赖注入库 wire

本文从“术”层面,讲述“依赖注入”的实现,带你体会其对于整洁架构 & DDD 等设计思想的落地,起到的支撑作用。

[golang]在Gin框架中使用JWT鉴权

什么是JWT JWT,全称 JSON Web Token,是一种开放标准(RFC 7519),用于安全地在双方之间传递信息。尤其适用于身份验证和授权场景。JWT 的设计允许信息在各方之间安全地、 compactly(紧凑地)传输,因为其自身包含了所有需要的认证信息,从而减少了需要查询数据库或会话存储

golang如何使用指针灵活操作内存?unsafe包原理解析

本文将深入探讨Golang中unsafe包的功能和原理。同时,我们学习某种东西,一方面是为了实践运用,另一方面则是出于功利性面试的目的。所以,本文还会为大家介绍unsafe 包的典型应用以及高频面试题。

golang reflect 反射机制的使用场景

Go语言中的 reflect 包提供了运行时反射机制,允许程序在运行时检查和操作任意对象的数据类型和值。 以下是 reflect 包的一些典型使用场景: 1. 动态类型判断与转换:当需要处理多种类型的变量且具体类型直到运行时才能确定时,可以使用反射来检查变量的实际类型,并在可能的情况下进行类型转换。

golang sync.Map 与使用普通的 map 的区别

使用sync.Map与普通的Go map主要有以下几点区别: 1. 并发安全性 普通map: 在没有外部同步的情况下,不是并发安全的。在多goroutine访问时,如果没有适当的锁或其他同步机制保护,可能会导致数据竞争和未定义行为。 sync.Map: 是并发安全的。它内部实现了必要的同步机制,允许

golang sync.Once 保证某个动作仅执行一次的机制

type Once struct { done atomic.Uint32 m Mutex } 这段代码是 Go 语言标准库中 sync 包的一部分,定义了一个 Once 类型。Once 类型用于确保某个函数只被执行一次。它包含一个 done 原子类型和一个 Mutex 互斥锁。 done 表示动作