golang 易错点-slice copy

背景

切片的复制是一个容易出错的操作,先看几个的例子:

go 复制代码
func main() {
	listing1()
	listing2()
	listing3()
}

func listing1() {
	s := []int{1, 2, 3}

	f(s[:2])
	fmt.Println(s) // output: [1,2,10]
}

func listing2() {
	s := []int{1, 2, 3}
	sCopy := make([]int, 2)
	copy(sCopy, s)

	f(sCopy)
	fmt.Println(sCopy) // output: [1,2]
	result := append(sCopy, s[2])
	fmt.Println(result) // output: [1,2,3]
}

func listing3() {
	s := []int{1, 2, 3}
	f(s[:2:2])
	fmt.Println(s) // output: [1,2,3]
}

func f(s []int) []int {
	return append(s, 10)
}
  • listing1 原切片 s 的 len=3,然后创建了一个 len=2 的临时切片传入 f,当 f 对它 append 新数据后,原切片 s 中的元素被覆盖了

  • listing2 将原切片复制到一个 len=2 的新切片 sCopy,将 sCopy 传入 f,这时 f 中的操作没有影响外面的原切片 sCopy

  • listing3 创建一个 len=2, cap=2 的临时切片传入 f,f 中的操作同样没用影响外面的原切片 s

为什么 listing1 中的原始数据会被覆盖,listing2,listing3 则不会?

知识点

知识点1:切片的数据结构

go 复制代码
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

slice 的底层其实是借助数组实现的,数据都保存在数组中。 array 就是指向这个数组的指针,len 代表切片的长度,cap 代表切片的容量。

知识点2:append 操作发现切片容量不足,会创建一个新的底层数组,同时把原数组的内容复制到新数组,再更新切片的 array 指针指向新数组,len,cap 都更新为新数组的长度和容量。

知识点3:go 函数采用值复制(pass-by-value),函数执行前会把参数复制一份,函数体中操作的是复制后的数据。对于指针来说,复制并不会复制底层数据,所以复制后新旧指针指向的是同一份数据。

回看开头的例子

listing1

go 复制代码
func listing1() {
	s := []int{1, 2, 3}

	f(s[:2])
	fmt.Println(s) // output: [1,2,10]
}

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

f 执行 append 操作前,两个切片的数据结构如图,:

lua 复制代码
       s (main)                           s (in f)
+-------+                            +-------+
|  ptr  |--------> +---+---+---+ <---|  ptr  |
|  len  | 3        | 1 | 2 | 3 |     |  len  | 2
|  cap  | 3        +---+---+---+     |  cap  | 3
+-------+          ▲                 +-------+
                   |
                   +-- Shared starting point

执行 append 操作,因为 f 中切片的 len=2,cap=3,go 判断底层数组可以容纳新元素,于是在原数组添加。这导致原数组的最后一个元素就被覆盖了:

lua 复制代码
       s (main)                           s (in f)
+-------+                            +-------+
|  ptr  |--------> +---+---+---+ <---|  ptr  |
|  len  | 3        | 1 | 2 | 10|     |  len  | 3
|  cap  | 3        +---+---+---+     |  cap  | 3
+-------+          ▲                 +-------+
                   |
                   +-- Shared starting point

listing2

go 复制代码
func listing2() {
	s := []int{1, 2, 3}
	sCopy := make([]int, 2)
	copy(sCopy, s)

	f(sCopy)
	fmt.Println(sCopy) 
	result := append(sCopy, s[2])
	fmt.Println(result)
}

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

首先 s 和 sCopy 是两个独立的切片,它们的底层数组不同,当执行 copy(sCopy, s) 后,数据结构如图:

lua 复制代码
     s (main)                             sCopy (main)
+-------+                            +-------+
|  ptr  |--------> +---+---+---+      |  ptr  |--------> +---+---+
|  len  | 3        | 1 | 2 | 3 |      |  len  | 2        | 1 | 2 |  <-- 独立数组2
|  cap  | 3        +---+---+---+      |  cap  | 2        +---+---+
+-------+          ^                  +-------+          ^
                   |                                     |
                   +-- 独立数组1                          +-- 独立数组2

f(sCopy) 会创建一个副本:

lua 复制代码
     sCopy (main)                       sCopy (in f)
+-------+                            +-------+
|  ptr  |--------> +---+---+ <-------|  ptr  |
|  len  | 2        | 1 | 2 |         |  len  | 2
|  cap  | 2        +---+---+         |  cap  | 2
+-------+          ^                 +-------+
                   |
                   +-- 共享的底层数组

执行 append 操作,因为 f 中切片的 len=2, cap=2,不够添加。于是会创建一个新的底层数组,并把数据复制过去:

lua 复制代码
                              (append 返回的新切片)
                            +-------+
    (sCopy in f)            |  ptr  |---------> +---+---+---+---+
    +-------+               |  len  | 3         | 1 | 2 | 10| 0 | <-- append后创建的新数组
    |  ptr  |---> +---+---+ |  cap  | 4         +---+---+---+---+
    |  len  | 2   | 1 | 2 | +-------+
    |  cap  | 2   +---+---+
    +-------+     ^
                  |
             原始的底层数组

因为 f 中的操作是基于新数组,所以 main 中的原数组不受影响。main 中 fmt.Println(sCopy) 输出的仍是 [1,2]。result := append(sCopy, s[2]) 在原数组基础上创建新数组,添加新元素,输出[1,2,3]。

listing3

go 复制代码
func listing3() {
	s := []int{1, 2, 3}
	f(s[:2:2])
	fmt.Println(s)
}

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

s[:2:2] 的意思是基于原切片,创建一个 len=2,cap=2 的新切片:

lua 复制代码
        s (in main)                 s (in f)
    +-------+                   +-------+
    |  ptr  |---+-------------+-|  ptr  |
    |  len  | 3 |             | |  len  | 2
    |  cap  | 3 |             | |  cap  | 2
    +-------+   |             | +-------+
                |             |
                v             |
            +---+---+---+ <---+
            | 1 | 2 | 3 |  <-- 共享的底层数组
            +---+---+---+

append 操作,因为此时 f 中的切片 cap=2,不够容纳新元素,go 会分配一个新数组:

lua 复制代码
      s (in f)                               (append 返回的新切片)
+-------+                                +-------+
|  ptr  |--------> +---+---+---+          |  ptr  |--------> +---+---+---+---+
|  len  | 2        | 1 | 2 | 3 |          |  len  | 3        | 1 | 2 | 10| 0 |  <-- 新分配的底层数组
|  cap  | 2        +---+---+---+          |  cap  | 4        +---+---+---+---+
+-------+                                +-------+
                   ^
                   |
               共享的底层数组

因为 f 中的操作是基于新数组,所以 main 中的原数组不受影响。fmt.Println(s) 输出仍是[1,2,3]。

总结

  1. 假如新旧切片都指向同一个底层数组,且新切片的 len < cap。append 操作会修改原切片的底层数组

  2. 要想避免影响,可以通过 copy 复制一个新数组,或者用 full slice expression 限制 cap,两者都会在 append 操作时创建一个新底层数组

相关推荐
风象南1 天前
我把大脑开源给了AI
人工智能·后端
橙序员小站1 天前
Agent Skill 是什么?一文讲透 Agent Skill 的设计与实现
前端·后端
怒放吧德德1 天前
Netty 4.2 入门指南:从概念到第一个程序
java·后端·netty
雨中飘荡的记忆1 天前
大流量下库存扣减的数据库瓶颈:Redis分片缓存解决方案
java·redis·后端
阿里云云原生1 天前
5 分钟零代码改造,让 Go 应用自动获得全链路可观测能力
云原生·go
开心就好20251 天前
UniApp开发应用多平台上架全流程:H5小程序iOS和Android
后端·ios
悟空码字1 天前
告别“屎山代码”:AI 代码整洁器让老项目重获新生
后端·aigc·ai编程
小码哥_常1 天前
大厂不宠@Transactional,背后藏着啥秘密?
后端
奋斗小强1 天前
内存危机突围战:从原理辨析到线上实战,彻底搞懂 OOM 与内存泄漏
后端
小码哥_常2 天前
Spring Boot接口防抖秘籍:告别“手抖”,守护数据一致性
后端