Go的切片是什么?一些小细节和容易错的地方

Go的切片是什么?一些小细节和容易错的地方

最近用GO做算法题的时候,在使用切片的过程出现了一些错误,他这里跟Java中的列表有着明显的区别。

go 复制代码
var res [][]int
	for _, interval := range intervals {
        ...
		lastResInterval := res[len(res)-1]
		if interval[0] <= lastResInterval[1] {
			lastResInterval[1] = int(math.Max(float64(interval[1]), float64(lastResInterval[1])))
		} else {
			res = append(res, interval)
		}
	}

这里代码其实是可以正常运行的,可能出错的地方是这里:
lastResInterval := res[len(res)-1] 这里只是复制了切片内容,相当于指向同一数组,但是一旦出现扩容等机制改变了底层数组,就会导致他们指向的不是同一个数组。

使得这里lastResInterval[1] = ...赋值产生差错

下面重新理解一下切片是什么:

理解 Go 的切片 (Slice) 到底是什么

在很多语言中,数组或列表就是一个连续的内存块。但在 Go 中,切片本身并不是数据,而是一个描述数据的"小本本",我们称之为切片头(Slice Header)。

这个"小本本"上记录了三件事:

  1. 指针 (Pointer):指向一个底层数组 (Underlying Array) 的某个元素的内存地址。这个底层数组才是真正存储数据的地方。
  2. 长度 (Length):切片中当前包含的元素个数。len() 函数看到的就是它。
  3. 容量 (Capacity):从指针指向的位置开始,到底层数组的末尾,总共能容纳多少个元素。cap() 函数看到的就是它。

所以,请记住核心:切片是一个带有指针、长度和容量的结构体。它和真正的数据(底层数组)是分开的。

类比:书的目录

你可以把底层数组想象成一本很厚的书,里面的内容就是你的数据。

而切片就像这本书的一个目录条目,它告诉你:

  • 指针 -> "从第 85 页开始看"
  • 长度 -> "这个章节总共 10 页" (从 85 页到 94 页)
  • 容量 -> "从 85 页到书的最后一页(比如 300 页)总共有 216 页的空间"

当你把一个切片赋值给另一个变量时,比如 b := a,你做的不是复制整本书,你只是复制了这个目录条目。现在你有两个一模一样的目录条目,但它们都指向同一本书的同一页。

分析res := [][]int

现在我们把这个模型套用到你的 [][]int 类型的 res 变量上。

[][]int 是一个"切片的切片"。这意味着:

  • res 是一个外层切片。
  • 它的底层数组里存储的元素,不是 int,而是另一个切片头 ([]int)。

假设 res[][]int{``{1, 5}, {8, 10}},它在内存中的结构是这样的:

复制代码
// res (外层切片头)
// 指针 -----> [ (内层切片头A) , (内层切片头B) ]  (res 的底层数组)
// 长度: 2       /                 \
// 容量: 2      /                   \
//             /                     \
// 指针A -----> [ 1, 5 ]          指针B -----> [ 8, 10 ]
// 长度A: 2                         长度B: 2
// 容量A: 2                         容量B: 2

这是一个两层的结构。res 的底层数组里放的不是数字,而是另外两个"小本本"(内层切片头 A 和 B)。

剖析关键代码 lastResInterval := res[len(res)-1]

现在,我们来执行这行代码。假设 res 里已经有 {{1, 5}} 这个元素了。

  1. res[len(res)-1]:这会获取 res 底层数组中的最后一个元素。这个元素是什么?它不是 {1, 5} 这个数据本身,而是指向 {1, 5} 的那个内层切片头。
  2. lastResInterval := ...:这行代码执行了赋值操作。根据我们第一步的原理,赋值一个切片就是复制它的切片头。

所以,执行完这行代码后,内存状态变成了:

复制代码
// res (外层切片头)
// 指针 -----> [ (内层切片头A) ] (res 的底层数组)
//                 |
//                 |
// 指针A ----------> [ 1, 5 ] (底层数据)


// lastResInterval (一个全新的切片头,是内层切片头A的副本)
// 指针A' ---------> [ 1, 5 ] (指向和上面完全相同的底层数据)

现在你有了两个切片头:

  • 一个是 res 底层数组里的那个(我们称之为原始头)
  • 另一个是 lastResInterval(我们称之为副本头)

重点来了:这两个切片头的指针,指向的是同一个底层数组 [1, 5]!

解开谜团:为什么修改可能会出错?

当你执行 lastResInterval[1] = 99 时:

  • Go 通过 lastResInterval 这个副本头找到它指向的底层数组 [1, 5]。
  • 然后修改了这个底层数组的第 1 个索引(0-based)的元素。
  • 底层数组变成了 [1, 99]。

因为 res 里的那个原始头也指向这个同一个底层数组,所以当你回头检查 res 时,你会发现 res 也变成了 {{1, 99}}。

那为什么我说这是错误的、危险的?

因为这个"成功"只是一个巧合。它只在你原地修改元素时才有效。如果你的操作涉及到了 append,并且导致了底层数组的重新分配,灾难就来了。

看这个例子

go 复制代码
// ... 假设 lastResInterval 指向 [1, 5],长度2,容量2
// 现在我们 append,容量不够了,Go 会创建一个新的、更大的底层数组
lastResInterval = append(lastResInterval, 100) 

执行 append 后:

  1. Go 发现 [1, 5] 这个底层数组容量不够。

  2. Go 会创建一个全新的、更大的数组,比如容量为 4。

  3. 把 [1, 5, 100] 复制到这个新数组里。

  4. lastResInterval 这个副本头的指针,现在会指向这个全新的数组! 它的长度和容量也会更新。

但是,res 里面的那个原始头呢?它毫不知情!它的指针仍然指向那个旧的、小的、内容是 [1, 5] 的底层数组!

从这一刻起,lastResInterval 和 res 内部的切片就分道扬镳了,它们指向了完全不同的内存地址。你对 lastResInterval 的任何后续修改都和 res 再无关系。

总结一下

  • 核心原理:切片赋值(b := a)和函数传参,都是复制切片头,而不是复制底层的数据。这使得多个切片头可以指向并共享同一个底层数组。

  • 原地修改的"假象":当你通过一个切片副本修改元素时(b[i] = x),你实际上是通过副本的指针修改了共享的底层数组,所以原始切片也能看到变化。

  • 真正的危险 (append):一旦你对副本使用了 append 并导致底层数组扩容,副本的指针就会指向一个新的底层数组,从而与原始切片"失联"。

  • 最佳实践:为了避免这种混乱和不可预知的行为,永远不要依赖于修改切片的副本来影响原始切片。始终通过最原始的变量来进行修改,这样代码的意图才清晰明确,且绝对安全。

错误的(脆弱的)

go 复制代码
last := res[len(res)-1]
last[1] = 99 // 碰巧能行
last = append(last, 100) // 绝对不行

正确的(健壮的)

go 复制代码
res[len(res)-1][1] = 99
res[len(res)-1] = append(res[len(res)-1], 100)
相关推荐
肖焱2 小时前
Java中的集合类有哪些?如何分类的?
后端
野生程序员y2 小时前
Spring DI/IOC核心原理详解
java·后端·spring
是萝卜干呀3 小时前
IIS 部署 asp.net core 项目时,出现500.19、500.31问题的解决方案
后端·iis·asp.net·hosting bundle
从零开始学习人工智能3 小时前
SpringBoot + Apache Tika:一站式解决文件数据提取难题
spring boot·后端·apache
IT_陈寒3 小时前
Python 3.12 的这5个新特性,让我的代码性能提升了40%!
前端·人工智能·后端
华仔啊3 小时前
别再被 Stream.toMap() 劝退了!3 个真实避坑案例,建议收藏
javascript·后端
夕颜1113 小时前
让 Cursor 教我写插件
后端
郭京京3 小时前
goweb内置的响应2
后端·go
小猪乔治爱打球3 小时前
[Golang 修仙之路] Go语言:内存管理
后端·面试