Go切片是值类型,其变量包含指向底层数组的指针、len和cap三个字段;赋值时复制这24字节,故s1和s2共享底层数组,修改内容会相互影响,但扩容后指针分离,互不影响。
声明一个 []int 变量时,它在栈上占据固定大小(通常 24 字节:指向底层数组的指针 + 长度 + 容量),赋值或传参时复制的是这 24 字节,不是整个底层数组。所以 s1 := []int{1,2,3}; s2 := s1 后修改 s2[0] = 99,s1[0] 也会变——这不是因为 s1 和 s2 是“同一个引用”,而是因为它们的指针字段指向同一块内存。
常见误解:看到“修改切片内容影响原切片”就认为它是引用类型。其实这是值类型携带指针导致的副作用。
Go 运行时中,切片头(slice header)定义为:
type slice struct {
arra
y unsafe.Pointer
len int
cap int
}
关键点:
array 是指向底层数组首地址的指针,不持有数组所有权len 是当前可读写长度,cap 是从 array 起始处开始的最大可用长度s = s[1:] 或 s = append(s, x),可能改变 len/cap,也可能触发扩容(分配新数组并拷贝)array 指针就和旧切片不同了,后续修改互不影响append 后原切片可能不受影响当 append 不触发扩容时(即 len ),新切片仍共享底层数组;一旦扩容(len == cap 且追加元素),运行时分配新数组,拷贝原数据,此时新切片与原切片完全独立。
示例:
orig := []int{1, 2}
a := orig
b := append(orig, 3) // 触发扩容 → b.array ≠ orig.array
orig[0] = 99
fmt.Println(orig) // [99 2]
fmt.Println(a) // [99 2]
fmt.Println(b) // [1 2 3] —— 不受影响
容易踩的坑:
append 后没返回,调用方看不到新增元素(因为扩容后指针已变)copy(dst, src) 会自动扩容 dst,实际只拷贝 min(len(dst), len(src)) 个元素,超出部分被丢弃不能靠打印切片值(fmt.Printf("%v", s))来判断,要对比底层指针:
func sameArray(a, b []int) bool {
return len(a) > 0 && len(b) > 0 && &a[0] == &b[0]
}
注意:&a[0] 在 len(a)==0 时 panic,需先判空;另外,即使 &a[0] == &b[0],也不代表整个数组都重叠——比如 a := s[0:2]; b := s[1:3],它们起始地址不同,但仍有重叠。
真正复杂的地方在于:切片行为既不像纯值类型(如 int)那样完全隔离,也不像 Java 的 List 那样有明确的引用标识。它的“半共享”特性必须结合 len/cap 和底层数组生命周期一起理解,稍不留神就会在并发或长期持有切片时引发意外修改。