Skip to main content

Go 切片传参:为什么 append 外部看不到?

这个问题非常经典,核心原因可以用一句话概括:

Go 函数传参是"值传递",你传进去的切片其实是一个"结构体的副本"。

虽然它们共享底层数组(指针一样),但它们拥有各自独立Len(长度)和 Cap(容量)。


1. 为什么 s[0]=x 外部能看到?

因为指针指向的是同一个地方。

当你传递切片时,Go 复制了那个"切片头"(SliceHeader):

  • 外部变量: {Ptr: 0x100, Len: 3, Cap: 5}
  • 函数内部变量: {Ptr: 0x100, Len: 3, Cap: 5}

注意 Ptr 都是 0x100。 函数执行 s[0] = 999 时,它顺着 0x100 这个地址去修改内存。

结果: 内存变了。外部变量也看 0x100,所以它看到了变化。


2. 为什么 append 外部看不到?

这是最坑的地方。append 会导致两种情况,但无论哪种,外部都看不到。

情况 A:容量足够(没发生扩容),但"视野"没跟上

假设 Len=3, Cap=5。 你 append 了一个元素,底层数组其实真的被修改了(第4个位置写入了数据)。

但是!SliceHeader 里的 Len 变了。

  • 函数内部变量: {Ptr: 0x100, Len: 4, Cap: 5} (长度变成了 4)
  • 外部变量: {Ptr: 0x100, Len: 3, Cap: 5} (长度依然是 3

结果: 虽然数据写进去了,但外部切片的 Len 锁死在 3。打印的时候,Go 只会遍历到 Len 为止。外部切片觉得"我只有 3 个元素",所以它看不见第 4 个元素。

情况 B:容量不够(发生了扩容),彻底分家

假设 Len=3, Cap=3。 你 append 了一个元素,原来的坑填不下了。

  • 函数内部: append 发现不够用,开辟了一块新内存地址 0x888,把老数据拷过去,再追加新数据。

  • 内部变量变成:{Ptr: 0x888, Len: 4, Cap: 6}

  • 外部变量: 依然指向老家。

  • 外部变量保持:{Ptr: 0x100, Len: 3, Cap: 3}

结果: 内部变量已经搬家了,外部变量还在看老房子。彻底无关了。


图解对比

我们可以把切片想象成一张"购物清单"

清单内容:

  1. 仓库地址:A区1号架
  2. 要拿几个商品:3个 (Len)

传递给函数时,你复印了一份清单给函数。

  • 操作 1:修改商品 (s[0] = x) 函数拿着复印件,去 A区1号架,把第一个苹果换成了香蕉。 你拿着原件,去 A区1号架,看到的自然也是香蕉。(生效)

  • 操作 2:追加商品 (append) 函数在复印件上把"要拿几个商品"从 3 改成了 4,并往架子上多放了一个梨。 你手里的原件,上面写的还是"要拿 3 个"。 当你去仓库取货时,你只拿前 3 个,完全不知道第 4 个梨的存在(不生效)


怎么解决?

如果你希望 append 也能被外面看到,必须把**更新后的清单(切片头)**传回给外面。

方法 1:返回新切片(最常用)

func addOne(s []int) []int {
s = append(s, 100)
return s // 把修改了 Len 的新切片头传出去
}

// 调用
list = addOne(list) // 外部用新切片覆盖旧切片

方法 2:传切片的指针 (Pointer to Slice)

这就相当于把"清单的原件"传进去了,而不是复印件。

func addOne(s *[]int) {
*s = append(*s, 100) // 直接修改外部那个切片头的 Len 和 Ptr
}

// 调用
addOne(&list)

相关阅读