Go 数组
数组长度属于类型,赋值与传参整体值拷贝;理解数组是掌握 slice 底层与固定大小值语义的基础。
[!info] 关联笔记
Go 数组
这个概念为什么会出现
序列数据需要“连续存放同类型元素”。许多语言把“可变长列表”做成默认;Go 则把层次拆开:
- 数组:长度固定,长度是类型的一部分,值语义
- 切片:长度运行时可变(在容量模型内),描述符语义
数组解决的是:编译期已知大小的值、明确的拷贝边界、以及 slice 的底层存储。日常业务代码更常写 slice,但若不理解数组,就无法真正理解 append、共享底层、以及 [32]byte 这类 API。
[!abstract] 一句话理解
[N]T是长度 N 的值类型序列;N属于类型,赋值/传参复制全部元素。slice 通过描述符指向数组(或数组片段),而不是“可增长数组类型本身”。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:RGB 调色板三元组(固定 3 槽)
设计工具里颜色常用固定三通道 (R,G,B),长度写进类型更安全:
[3]int 与 [4]int(RGBA)根本不是同一类型,不会被误传。
同时要搞清:
- 赋值/传参 = 整份拷贝
- 想原地改必须传
*[3]int color[:]得到 slice 视图,改视图会改回数组
package main
import "fmt"
// paintCopy 模拟“滤镜函数拿到颜色副本”。
// 业务:误以为能改调用方的 RGB;实际只改参数副本。
func paintCopy(rgb [3]int) {
rgb[0] = 100 // 只动副本
}
// paintInPlace 模拟“真正改画布上的颜色”。
// 教学点:传数组指针才能写回调用方。
func paintInPlace(rgb *[3]int) {
rgb[0] = 100
}
func main() {
// 零值数组:4 通道占位全 0
var emptyRGBA [4]int
// 固定文案三元组;[...] 仍是定长数组,不是 slice
label := [3]string{"go", "is", "fun"}
scale := [...]int{10, 20, 30}
fmt.Println(emptyRGBA, label, scale)
// 赋值 = 深拷贝元素,不是共享
original := [3]int{1, 2, 3} // 某像素 RGB
snapshot := original
snapshot[0] = 999
fmt.Println("original", original, "snapshot", snapshot) // [1 2 3] [999 2 3]
// 按值传参改不到外面
paintCopy(original)
fmt.Println("after paintCopy", original) // [1 2 3]
// 指针传参:原地改 R 通道
paintInPlace(&original)
fmt.Println("after paintInPlace", original) // [100 2 3]
// 数组 → slice 视图:共享底层存储
view := original[:]
view[1] = 7 // 改 G
fmt.Println("array", original, "view", view)
// 同类型且元素可比较时,数组可直接 ==
fmt.Println("equals?", original == [3]int{100, 7, 3})
}
建议运行:
go run .
期望输出含:after paintCopy 仍为 [1 2 3],after paintInPlace 为 [100 2 3],最终 equals? true。
结合场景再看四个关注点
-
长度属于类型
[3]int与[4]int不能互赋——RGB/RGBA 不会静默混用。 -
赋值与传参整体拷贝
快照、滤镜副本天然隔离;大数组要小心复制成本。 -
原地改用指针
paintInPlace(*[3]int)才写回画布。 -
[:]桥到 slice
视图共享底层;多数 API 收 slice,固定缓冲仍可用数组再切片。
核心概念与准确模型
类型里有长度
var a [3]int
var b [4]int
// a = b // 编译错误:不同类型
规范:Array types
长度必须是非负常量(编译期常量表达式)。n := 3; var a [n]int 非法——要变长用 slice。
值语义
| 操作 | 行为 |
|---|---|
| 赋值 | 复制全部元素 |
| 函数参数 | 复制全部元素 |
| 返回 | 复制全部元素 |
取址 &a | 得到 *[N]T,可共享修改 |
大数组按值传递成本高;是否改用 *[N]T 或 slice 取决于所有权与测量。
零值
var a [N]T → 每个元素为 T 的零值。无“未初始化垃圾值”语义。
字面量
a := [3]int{1, 2, 3}
b := [3]int{1, 2} // 第三元素 0
c := [...]int{1, 2, 3} // 编译器数出 N=3
d := [5]int{1: 10, 3: 30} // 索引初始化
[...]T{...} 不是运行时动态长度,只是省略手写 N。
比较
若元素类型可比较,则同类型数组可用 == / != 逐元素比较。
slice 不可直接用 ==(仅能与 nil 比)。这是数组与 slice 的实用差异之一。
与 slice 的关系
数组: 值本身就是全部元素
slice: (ptr, len, cap) 指向某数组片段
arr := [5]int{0, 1, 2, 3, 4}
s1 := arr[1:4] // len=3, 底层是 arr
s2 := arr[:] // 看整个数组
要点:
- 从数组切出的 slice 在 cap 允许时
append可能写回原数组后续槽位。 - 需要独立数据时
copy到新 slice。 - 函数参数写
[]T几乎总是比写[N]T更灵活;[N]T适合大小即类型契约。
详见 slice。
典型用得上数组的地方
- 密码学摘要:
[32]byte - IPv4:
[4]byte(或netip等更高层类型) - 固定窗口、查表、小而稳的值对象
- 需要值比较/作 map key 的固定序列(元素可比较)
range
for i, v := range arr {
// v 是元素副本
}
改 v 不影响 arr;改元素请用 arr[i] 或指针。更多见 go-range-semantics。
设计动机
- 类型即大小
避免“长度写在别处”的一类错误。 - 值拷贝边界清晰
小固定数据可安全复制,无需共享。 - 为 slice 提供底层
运行时与编译器对连续内存的基础模型。
边界情况与反直觉行为
[0]T合法
零长度数组;有时用于对齐/占位技巧,日常少见。- 多维
[2][3]int是“数组的数组”,不是数学矩阵类型系统。 - 与指针数组
[N]*T复制的是指针,不深拷贝T。 - 反射与泛型
不同N是不同类型,抽象时常退化到 slice。 - 栈/堆
是否在栈上由逃逸分析决定,不是数组的语言保证。
常见误区
[!warning] 常见误区:把 Go 数组当成 C 数组传参退化 错误:以为函数参数里的数组自动变指针。
正确:Go 按值复制整个数组;要共享请显式*[N]T或 slice。
[!warning] 常见误区:
[...]T是动态数组 错误:当成运行时可变长。
正确:编译期确定长度的语法糖。
[!warning] 常见误区:业务 API 到处用数组 错误:只因“连续内存”就在公开 API 写死
[256]byte缓冲协议而不必要。
正确:公开面优先 slice;数组用于真正固定且属于类型契约的值。
[!warning] 常见误区:从数组切片后随意 append 错误:忘记可能覆盖原数组后续元素。
正确:截断 cap(a[:n:n])或先 copy。
工程实践
- 固定哈希/ID 用数组表达“恰好 N 字节”。
- 内部小缓冲 可用数组,再
buf[:]交给期望 slice 的 API。 - 比较与 map key 优先可比较的数组值,而非 slice。
- 性能 大数组避免无谓拷贝;先测再优化。
- 教学顺序 先数组值语义,再讲 slice 描述符,坑更少。
可验证实验
[3]int与[4]int赋值是否编译通过。- 按值传参修改是否影响调用方。
a[:]修改是否反映到a。a == b与s == t(slice)编译差异。append(a[:2:2], 9)是否改写a[2](对比不截断 cap)。
本节总结
- 本质:长度在类型中的值类型序列。
- 关键规则:整体拷贝、可比较(元素允许时)、是 slice 的底层。
- 日常:多用 slice;数组用于固定大小契约。
- 最易错:C 退化错觉、
...动态错觉、切片共享踩坑。 - 下一步:slice 的 len/cap/append 模型。
自测题
概念题
- 为什么
[3]int与[4]int不能互相赋值? - 数组传参的默认语义是什么?
- 数组与 slice 谁可以直接用
==?
代码推理题
a := [3]int{1, 2, 3}
s := a[:2]
s = append(s, 9)
fmt.Println(a, s)
在默认 cap 来自原数组时,a 可能变成什么?
工程思考题
函数要接收“恰好 16 字节的 ID”,参数写 [16]byte 还是 []byte?各有何利弊?
参考答案
展开
- 长度是类型的一部分,二者类型不同。
- 复制整个数组到形参。
- 同类型可比较数组可以;slice 不能与另一个 slice 用
==(只能与 nil)。
代码:s的 cap 通常延伸到a末尾,append可能写a[2]=9,于是a为[1 2 9],s为[1 2 9](实现符合 cap 规则时)。
工程:[16]byte强制长度、可比较、拷贝清晰;[]byte更灵活但要校验len==16,且可能共享底层。密钥/ID 常用数组或把校验封进类型。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Spec — Array types | 规范 | 类型与长度 |
| Spec — Slice types | 规范 | 与数组关系 |
| Go Slices: usage and internals | 博客 | 底层数组视角 |
| Effective Go — Arrays | 文档 | 用法对照 |
笔记元信息
- 建议文件名:
go-arrays.md - 所属阶段:阶段一
- 本篇状态:已深化
- 建议下一篇:Go Slice