Go 数组
Go 中固定长度、值类型的序列容器,长度是类型的一部分,赋值和传参均发生拷贝。
#type / concept
#status / growing
#tech / dev
#resource / go
[!info] related notes
- 所属 MOC: Go 语言基础 MOC
- 前置概念: Go 基本类型
- 并列概念: Go 切片
Go 数组
一句话定义
数组是 Go 中长度固定、元素类型相同、以值语义存储的连续内存序列,其长度是类型的一部分。
核心机制 / 工作原理
Go 数组与 C 数组在内存布局上相似——一块连续的内存区域——但语义完全不同:
-
长度是类型的一部分:
[3]int和[4]int是两个完全不同的类型,不能互相赋值,也不能比较。这意味着无法编写一个函数同时处理不同长度的数组。 -
值语义:数组赋值、传参、返回时都会完整复制一份。修改副本不影响原始数组。
-
零值初始化:未显式初始化的数组,其每个元素都是对应类型的零值(数值为 0,字符串为
"",bool 为false,指针/切片/map/channel 为nil)。 -
比较运算:相同类型(长度相同 + 元素类型相同且可比较)的数组可以用
==和!=比较,逐元素对比。 -
与 slice 的关系:
arr[:]可以从数组创建一个引用原内存的切片。切片(slice)在实践中远比数组常用,数组常用于底层存储或特定的固定大小场景。
最小例子 / 最小场景
package main
import "fmt"
func main() {
// 声明与初始化
var a [5]int // 零值数组: [0 0 0 0 0]
b := [3]string{"go", "is", "fun"} // 字面量初始化
c := [...]int{10, 20, 30} // 编译器自动推断长度为 3
// 值语义: 赋值产生副本
original := [3]int{1, 2, 3}
copy := original
copy[0] = 999
fmt.Println(original) // [1 2 3] — 不受影响
fmt.Println(copy) // [999 2 3]
// 长度是类型的一部分
// var d [4]int = original // 编译错误: cannot use original (type [3]int) as type [4]int
// 比较
fmt.Println([3]int{1, 2, 3} == [3]int{1, 2, 3}) // true
fmt.Println([3]int{1, 2, 3} == [3]int{1, 2, 4}) // false
// 数组转切片
s := b[:]
fmt.Println(s, len(s)) // [go is fun] 3
}
为什么重要
- 性能可控:数组在栈上分配(小数组时),无需堆分配和 GC 压力,适合对性能极度敏感的热路径。
- 固定大小协议:当数据大小在编译期已知且不可变时(如 IP 地址
[4]byte、[16]byteUUID、哈希值[32]byte),数组语义更精确。 - 理解 slice 的基础:slice 的底层就是一个指向数组的指针 + 长度 + 容量,理解数组才能真正理解 slice。
- 安全的值拷贝:在并发场景中,将小数组作为参数传入函数,天然得到一份不可变副本,无需额外加锁。
边界与易混淆点
- 不能用变量指定数组长度:
n := 5; var a [n]int在 Go 中是非法的(长度必须是编译期常量表达式),需要使用 slice。 - 大数组值拷贝开销大:
[1000000]int作为函数参数会复制 8MB 内存,实际开发中几乎总用 slice 或指针*[N]T来避免。 [...]T不是动态长度:[...]int{1,2,3}只是让编译器在编译期数出元素个数,结果仍然是固定长度数组[3]int。- 数组是可比较的,slice 不是:这是两者的重要区别之一,
[]int{1,2} == []int{1,2}会编译报错。 - range 遍历数组得到的是副本:
for i, v := range arr中v是元素的副本,修改v不影响原数组。