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的内容被更新了,尽管我们没有直接对s1[2]或者s2[1]进行赋值操作。需要牢记这种操作可能引发意料之外的结果。

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:限制切片的容量

采用所谓的完整切片表达式(s[low:high:max]). 该表达式与创建切片 s[low: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操作会修改原切片。如果不想对原切片有影响,可以采用上面深拷贝或完整表达式的方法阻止副作用的产生。

相关推荐
qq_589568105 分钟前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
小蜗牛慢慢爬行32 分钟前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
黑客老陈1 小时前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安1 小时前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
wm10431 小时前
java web springboot
java·spring boot·后端
暴富的Tdy2 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se2 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235612 小时前
web 渗透学习指南——初学者防入狱篇
前端