数组是固定长度值类型,直接存储数据;切片是24字节头结构(指针、len、cap)加分离底层数组,属引用类型,支持动态扩容与共享。

数组和切片在内存中根本不是一回事:数组是数据本身,切片只是三个字段组成的头结构 + 底层数组(通常在堆上)。
数组的内存布局:连续、固定、值语义
声明 var arr [4]int 后,整个数组内容(4个 int)就直接落在当前作用域的栈上(小数组)或堆上(大数组),地址连续,长度不可变。访问 arr[2] 时,编译器直接算出偏移量:&arr + 2 * unsafe.Sizeof(int(0))。
常见错误现象:
- 把大数组(如
[1024 * 1024]byte)作为函数参数传入 → 栈溢出或严重性能损耗 - 误以为
arr[:]是“转成切片就安全了” → 实际仍共享同一块内存,修改切片会改原数组
关键点:
立即学习“go语言免费学习笔记(深入)”;
-
len(arr)和cap(arr)相等,且为编译期常量 - 数组赋值(
b := arr)是完整拷贝,b[0] = 999不影响arr - 多维数组(如
[3][4]int)仍是单块连续内存,不是指针数组
切片的内存布局:头结构 + 分离底层数组
声明 s := []int{1,2,3} 或 s := make([]int, 3, 5) 时,变量 s 本身只占 24 字节(64 位系统):一个 unsafe.Pointer(指向底层数组)、一个 len int、一个 cap int。真实数据存在别处(通常是堆)。
使用场景:
- 需要动态增删元素 → 必须用切片,数组做不到
- 函数间传递大量数据 → 切片只传 24 字节头,开销极小
- 共享子序列(如日志缓冲区切片)→ 多个切片可共用同一底层数组
容易踩的坑:
-
append后原切片变量的ptr可能已失效(扩容触发新底层数组分配) -
s[0:0]或s[:0]不清空数据,只重置len;底层数组内容还在,可能被后续append复用 → 引发脏数据 - 从同一个数组创建多个切片后并发写入 → 数据竞争,
go build -race会报错
为什么 append 有时不扩容,有时却复制整块内存?
append 的行为完全取决于当前切片的 cap 是否足够容纳新增元素。它不会“预分配一倍”,而是按需增长(实际策略是:小于 1024 时翻倍,大于等于 1024 时每次 +25%)。
示例:
sl := make([]int, 2, 4) sl = append(sl, 3) // len=3, cap=4 → 未扩容,ptr 不变 sl = append(sl, 4, 5) // len=5 > cap=4 → 新分配数组,复制旧数据,ptr 改变
性能影响:
- 频繁小量
append(如循环中逐个加)→ 多次扩容 + 复制,O(n²) 时间复杂度 - 提前用
make([]T, 0, expectedCap)预估容量 → 避免中间扩容,O(n) 稳定 - 对只读切片做
append并忽略返回值 → 原切片不变,但新切片被丢弃,内存白分配
如何验证内存布局差异?
用 unsafe 或 reflect 查看底层字段最直接。例如打印切片头:
import "unsafe"
s := []int{1,2,3}
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("ptr=%p, len=%d, cap=%d\n", hdr.Data, hdr.Len, hdr.Cap)注意:
- 不要在生产环境依赖
unsafe操作切片头,Go 1.21+ 对某些操作有更严格检查 - 用
fmt.Printf("%p", &s[0])看的是第一个元素地址,不是切片头地址 - 数组取地址(
&arr)得到的是整个数组起始地址;切片取地址(&s)得到的是切片头地址,和&s[0]完全不同
真正容易被忽略的是:切片的“引用性”不等于“安全性”——它既带来共享便利,也带来隐式耦合。任何对底层数组的修改(包括其他切片、原始数组、甚至 unsafe 指针)都可能影响当前切片,而这种影响往往没有编译期提示。


















