Go语言中常见100问题-#25 append操作陷阱与解决方法

前言

append函数是Go语言中操作切片高频函数,但是append函数如果使用不当在某些情况下将会产生意料之外的结果,本文将通过案例详细分析问题原因及解决方法。

案例引入

下面来看一个具体的例子,先初始化一个切片s1,基于s1创建切片s2,最后对s2执行append操作创建切片s3.

golang 复制代码
s1 := []int{1, 2, 3}
s2 := s1[1:2]
s3 := append(s2, 10)

运行上述程序后,你知道s1、s2和s3的状态是什么样的吗?先不要看下面的分析,看看自己是否答对。

原因分析

下面通过图解的形式进行说明,在执行第二条语句后,s1和s2在内存中的状态如下图所示。

s1是一个长度为3,容量为3的切片,s2是一个长度为1,容量为2的切片,s1和s2底层共用同一个数组。通过append向切片里面添加元素的时候,会检查切片是否满了(length==capacity). 如果没有满,则底层数组的下一个索引位置添加元素,并将切片的长度加1. 在本例中,切片s2还未满,所以对它执行append操作后,s3在内存中的状态如下。

底层数组中的最后一个元素的值被更新为10,打印s1、s2和s3得到输出内容如下。切片s1的内容被更新了,尽管我们没有直接对s12或者s21进行赋值操作。需要牢记这种操作可能引发意料之外的结果。

console 复制代码
s1=[1 2 10], s2=[2], s3=[2 10]

切片传给函数后进行append操作会引发什么问题?

下面来看将切片传递给函数产生的效果。切片s有3个元素,然后截取前2个值传给函数f. 如果在函数f中更新s的值,main函数中也会感知到s的变化。

golang 复制代码
func main() {
    s := []int{1, 2, 3}
    f(s[:2])
    // Use s
}

func f(s []int) {
    // Update s
}

如果在函数f中执行append操作,将会更新s中的第三个元素,尽管传递给f的只是前两个值。例如下面的程序,main函数中打印s的值,输出的是 1 2 10

golang 复制代码
func main() {
    s := []int{1, 2, 3}
    f(s[:2])
    fmt.Println(s) // [1 2 10]
}

func f(s []int) {
    _ = append(s, 10)
}

如果我们想防御性保护第三个元素不被函数f修改,有两种处理方法。

解决方法1:采用深拷贝

传递给函数f的切片与main函数中的切片彻底隔离。实现代码如下。sCopy切片是对切片s的深拷贝。

golang 复制代码
func main() {
    s := []int{1, 2, 3}
    sCopy := make([]int, 2)
    copy(sCopy, s)
    f(sCopy)
    result := append(sCopy, s[2])
    // Use result
}

func f(s []int) {
    // Update s
}

在函数f内部对s进行操作不会影响到main函数中的切片s,因为它们是完全不同的两个切片。这种方法的缺点是代码比较复杂阅读较困难,并且需要进行深拷贝操作,当要拷贝的切片很大时是一个问题。

解决方法2:限制切片的容量

采用所谓的完整切片表达式(slow:high:max). 该表达式与创建切片 slow:high类似,唯一的不同是设置切片的容量为 max-low. 具体的实现如下:

golang 复制代码
func main() {
    s := []int{1, 2, 3}
    f(s[:2:2])
    // Use s
}

func f(s []int) {
    // Update s
}

传递给函数f的切片不是s:2而是s:2:2,容量为2(2-0),在内存中的状态如下图。当在函数f中向里面append元素时,因为切片已满会进行扩容操作,在新的切片上添加元素而不会影响main函数。

总结

在使用切片的时候,要留意不当操作可能引发的副作用。向切片里面添加元素时,如果切片未满,append操作会修改原切片。如果不想对原切片有影响,可以采用上面深拷贝或完整表达式的方法阻止副作用的产生。

相关推荐
大家的林语冰4 分钟前
✌️ 让 Rust 再次伟大,pnpm 12 抛弃 TypeScript,移植 Rust 原地起飞!
前端·javascript·node.js
程序员cxuan7 分钟前
本地跑一个 Qwen 3.8,你将拥有一个 Opus 4.6
人工智能·后端·程序员
凤山老林1 小时前
高保真集成测试:Spring Boot 结合 Testcontainers 的工程落地
spring boot·后端·集成测试
步行cgn1 小时前
MyBatis <trim> 标签完全解析:动态 SQL 的终极武器
后端
XPoet1 小时前
AI 编程工程化:实战——从 0 到 1 搭建 AI 编程工作流
前端·后端·ai编程
掘金者阿豪1 小时前
向量数据库不是终点,企业AI真正需要的是融合数据库
后端
程序员cxuan1 小时前
OpenAI Linux 版来了!
人工智能·后端·程序员
大黄评测1 小时前
Angular 表单:响应式表单高级用法,Typed Forms 类型化表单实战
后端
大勇前进1 小时前
Angular 变更检测深度讲解:OnPush 策略什么时候用、踩过哪些坑
后端
拾光师1 小时前
Python 解析 JSON 日志:从一行数据到一份报告
后端