Go 数组

数组长度属于类型,赋值与传参整体值拷贝;理解数组是掌握 slice 底层与固定大小值语义的基础。

#type / concept #status / growing #tech / dev #resource / go

[!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)根本不是同一类型,不会被误传。

同时要搞清:

  1. 赋值/传参 = 整份拷贝
  2. 想原地改必须传 *[3]int
  3. 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

结合场景再看四个关注点

  1. 长度属于类型
    [3]int[4]int 不能互赋——RGB/RGBA 不会静默混用。

  2. 赋值与传参整体拷贝
    快照、滤镜副本天然隔离;大数组要小心复制成本。

  3. 原地改用指针
    paintInPlace(*[3]int) 才写回画布。

  4. [:] 桥到 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[:]      // 看整个数组

要点:

  1. 从数组切出的 slice 在 cap 允许时 append 可能写回原数组后续槽位。
  2. 需要独立数据时 copy 到新 slice。
  3. 函数参数写 []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

设计动机

  1. 类型即大小
    避免“长度写在别处”的一类错误。
  2. 值拷贝边界清晰
    小固定数据可安全复制,无需共享。
  3. 为 slice 提供底层
    运行时与编译器对连续内存的基础模型。

边界情况与反直觉行为

  1. [0]T 合法
    零长度数组;有时用于对齐/占位技巧,日常少见。
  2. 多维
    [2][3]int 是“数组的数组”,不是数学矩阵类型系统。
  3. 与指针数组
    [N]*T 复制的是指针,不深拷贝 T
  4. 反射与泛型
    不同 N 是不同类型,抽象时常退化到 slice。
  5. 栈/堆
    是否在栈上由逃逸分析决定,不是数组的语言保证。

常见误区

[!warning] 常见误区:把 Go 数组当成 C 数组传参退化 错误:以为函数参数里的数组自动变指针。
正确:Go 按值复制整个数组;要共享请显式 *[N]T 或 slice。

[!warning] 常见误区:[...]T 是动态数组 错误:当成运行时可变长。
正确:编译期确定长度的语法糖。

[!warning] 常见误区:业务 API 到处用数组 错误:只因“连续内存”就在公开 API 写死 [256]byte 缓冲协议而不必要。
正确:公开面优先 slice;数组用于真正固定且属于类型契约的值。

[!warning] 常见误区:从数组切片后随意 append 错误:忘记可能覆盖原数组后续元素。
正确:截断 cap(a[:n:n])或先 copy。

工程实践

  1. 固定哈希/ID 用数组表达“恰好 N 字节”。
  2. 内部小缓冲 可用数组,再 buf[:] 交给期望 slice 的 API。
  3. 比较与 map key 优先可比较的数组值,而非 slice。
  4. 性能 大数组避免无谓拷贝;先测再优化。
  5. 教学顺序 先数组值语义,再讲 slice 描述符,坑更少。

可验证实验

  1. [3]int[4]int 赋值是否编译通过。
  2. 按值传参修改是否影响调用方。
  3. a[:] 修改是否反映到 a
  4. a == bs == t(slice)编译差异。
  5. append(a[:2:2], 9) 是否改写 a[2](对比不截断 cap)。

本节总结

  • 本质:长度在类型中的值类型序列。
  • 关键规则:整体拷贝、可比较(元素允许时)、是 slice 的底层。
  • 日常:多用 slice;数组用于固定大小契约。
  • 最易错:C 退化错觉、... 动态错觉、切片共享踩坑。
  • 下一步slice 的 len/cap/append 模型。

自测题

概念题

  1. 为什么 [3]int[4]int 不能互相赋值?
  2. 数组传参的默认语义是什么?
  3. 数组与 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?各有何利弊?

参考答案

展开
  1. 长度是类型的一部分,二者类型不同。
  2. 复制整个数组到形参。
  3. 同类型可比较数组可以;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
创建于 2026/6/25 更新于 2026/7/15