数组是值类型,切片是含指针、len、cap的结构体;赋值和传参时数组复制全部元素,切片仅复制24字节结构体;append可能扩容导致底层数组更换,影响共享数据。

数组是值类型,切片只是三个字段的结构体
声明 var a [3]int 时,Go 直接在栈上分配 3 个 int 的连续空间;而 s := []int{1,2,3} 实际只生成一个 24 字节的结构体(64 位系统下:8 字节指针 + 8 字节 len + 8 字节 cap),它不存数据,只“看”底层数组。
- 赋值
b := a→ 复制全部 3 个整数;t := s→ 只复制那 24 字节,s和t仍指向同一底层数组 - 传参时:
func f(a [1000]int)每次调用都压栈 1000 个整数;func g(s []int)只传 24 字节,且s[0] = 123会真实改到底层数组 - 零值差异:
var x [3]int是[0 0 0],可直接读写;var y []int是nil,len(y)为 0,y[0]会 panic
切片扩容时可能悄悄换掉底层数组
append 不一定复用原数组——是否扩容,取决于 len(s) == cap(s)。一旦触发扩容,Go 会分配新底层数组、拷贝旧数据、再追加,此时原切片和其他共享该底层数组的切片就“断连”了。
- 常见坑:
arr := [3]int{1,2,3}; s1 := arr[:2]; s2 := arr[1:3],然后s1 = append(s1, 99)→ 因cap(s1) == 3,未扩容,s2[0]突变为 99 - 但若
arr更长(如[5]int),s1的cap变成 5,append后不扩容,行为更隐蔽 - 扩容策略:
cap 时翻倍;cap >= 1024时增加 25%,实际申请容量还可能向上对齐
截取切片会共享底层数组,不是安全拷贝
a[1:3] 这类操作不会分配新内存,只是调整结构体里的 ptr 和 len,cap 也会变(从起点算到底层数组末尾)。这意味着修改子切片,可能意外污染原始数据。
- 想真正隔离:用
copy手动复制,或make新切片再copy:newS := make([]int, len(old)); copy(newS, old) - 避免副作用:对来自大数组的子切片做
append前,先确认cap是否足够;否则考虑预分配或显式拷贝 - 注意
&s[0]不等于底层数组首地址全貌——无法靠它安全推导数组边界,越界访问仍 panic
函数内 append 不影响调用方的切片变量
虽然切片是“引用类型”,但传参仍是值传递:传递的是那个 24 字节结构体的副本。所以函数内部重赋值 s = append(s, x) 若触发扩容,新 ptr 只在函数内生效;原调用方的 s 仍指向旧底层数组,len 和 cap 也不变。
立即学习“go语言免费学习笔记(深入)”;
- 真正影响调用方的,只有对已有元素的修改:
s[0] = 123或s = s[:len(s)-1](缩短) - 想让扩容效果“返回”,必须显式返回新切片:
return append(s, x),并由调用方重新赋值 - 这个细节常被面试官追问:为什么
append要返回切片?答案就在这里——结构体副本里的ptr可能已变
ptr 可能在任意一次 append 后悄然切换到一块全新的内存。所谓“引用”,不是靠语言层面的引用类型实现的,而是靠结构体里那个隐式指针字段——而这个字段,随时可能被运行时替换成另一个。










