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 操作时创建一个新底层数组

相关推荐
这里有鱼汤35 分钟前
📊量化实战篇:如何计算RSI指标的“拥挤度指标”?
后端·python
魔术师卡颂43 分钟前
不就写提示词?提示词工程为啥是工程?
前端·人工智能·后端
程序员清风1 小时前
快手二面:乐观锁是怎么用它来处理多线程问题的?
java·后端·面试
IT_陈寒1 小时前
《Redis性能翻倍的7个冷门技巧,90%开发者都不知道!》
前端·人工智能·后端
一线大码1 小时前
SpringBoot 优雅实现接口的多实现类方式
java·spring boot·后端
PFinal社区_南丞2 小时前
构建可维护的正则表达式系统-pfinal-regex-center设计与实现
后端·php
Imnobody2 小时前
吴恩达 Prompt 工程课精讲②:写出高可靠 Prompt 的2大黄金法则
后端
yuuki2332332 小时前
【C语言】程序的编译和链接(基础向)
c语言·后端
梅小西爱学习2 小时前
线上CPU飙到100%?别慌,这3个工具比top快10倍!
java·后端·cpu
radient2 小时前
属于Agent的课本 - RAG
人工智能·后端·程序员