Appearance
04|数组
数组是长度固定的同类型元素序列。Go 的数组和 C 语言类似,但有一个重要区别:数组是值类型。把数组赋给另一个变量、或把数组传给函数时,会复制整个数组的内容。这个特性在传递大数组时会导致明显的性能开销,也是理解切片(下一讲的内容)为什么存在的背景。
一、声明与初始化
数组声明时需要指定长度和元素类型:
go
package main
import "fmt"
func main() {
var hosts [3]string
hosts[0] = "web01"
hosts[1] = "web02"
hosts[2] = "db01"
fmt.Println(hosts[0]) // web01
fmt.Println(len(hosts)) // 3
}声明时直接初始化:
go hosts
让编译器数元素个数:
go hosts
数组的长度是类型的一部分。[3]string 和 [5]string 是两种不同的类型,不能互相赋值:
go
var a [3]string
var b [5]string
// a = b // 编译错误:cannot use b as [3]string这个设计让编译器能在编译期就确定数组的内存布局,但也限制了数组的灵活性。
二、遍历
用 for 索引遍历:
go for
fmt.Println(i, hosts[i])
}用 range 遍历(推荐):
go for
fmt.Println(index, host)
}range 遍历数组时返回索引和元素的副本。修改副本不会影响原数组:
go for
host = "new-" + host // 只修改了副本
}
fmt.Println(hosts) // 原数组不变需要修改原数组时,用索引访问:
go for
hosts[i] = "new-" + hosts[i]
}三、值类型特性
数组是值类型,赋值和传参都会复制整个数组:
go
func modifyHost(h [3]string) {
h[0] = "proxy01" // 修改的是副本
}
func main() {
hosts := [3]string{"web01", "web02", "db01"}
modifyHost(hosts)
fmt.Println(hosts[0]) // 仍然是 web01
}modifyHost 收到的是 hosts 的完整副本,函数内部修改不影响原数组。大数组的复制开销明显——一个包含 1000 个结构体的数组,传参时会复制 1000 个结构体的全部字段。
如果需要让函数修改原数组,传数组指针:
go
func modifyHost(h *[3]string) {
(*h)[0] = "proxy01"
}
func main() {
hosts := [3]string{"web01", "web02", "db01"}
modifyHost(&hosts)
fmt.Println(hosts[0]) // proxy01
}不过 Go 里更常见的做法是直接用切片(下一讲),而不是传递数组指针。
四、数组比较
元素类型可比较的数组,可以用 == 和 != 直接比较:
go
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
fmt.Println(a == b) // true数组比较要求长度相同、元素类型相同、每个对应位置的元素都相等。包含切片或映射的数组不能直接比较。
五、多维数组
Go 支持多维数组,常用于矩阵或表格数据:
go
var matrix [3][3]int
matrix[0][0] = 1
matrix[1][1] = 1
matrix[2][2] = 1初始化:
go
matrix := [3][3]int{
{1, 0, 0},
{0, 1, 0},
{0, 0, 1},
}多维数组在内存中是连续存储的,matrix[0] 是一个 [3]int 数组,matrix[0][0] 是第一个元素。遍历多维数组用嵌套循环:
go for
for j := 0; j < len(matrix[i]); j++ {
fmt.Print(matrix[i][j], " ")
}
fmt.Println()
}六、数组与切片的区别
数组和切片看起来很像,但本质不同:
| 特性 | 数组 | 切片 |
|---|---|---|
| 长度 | 固定,编译期确定 | 可变,运行时动态增长 |
| 类型 | [N]T,长度是类型一部分 | []T,长度不是类型一部分 |
| 传递方式 | 值复制 | 引用传递(复制的是底层指针+长度+容量) |
| 使用场景 | 长度固定且小,需要值语义 | 长度不确定,需要动态增删 |
绝大多数场景下,切片比数组更灵活。数组主要用在两种地方:一是需要值语义的固定长度数据(如坐标点、颜色值),二是作为切片的底层存储。
七、常见错误
索引越界
go
hosts := [3]string{"web01"}
fmt.Println(hosts[5]) // panic: index out of bounds数组索引从 0 开始,最大有效索引是 len-1。Go 会在运行时检查索引边界,越界访问会 panic。生产代码中,索引值来自外部输入时,先检查范围:
go if
fmt.Println(hosts[index])
}用数组当函数参数处理大数据
go
func process(data [1000000]int) { // 复制了 100 万个整数
// ...
}大数组传参会复制大量数据,应该用切片或指针。除非数组很小(比如几个元素的坐标点),否则函数参数优先用切片。
把数组和切片混用
go
func printHosts(hosts []string) {
for _, h := range hosts { fmt.Println(h) }
}
var hosts [3]string
// printHosts(hosts) // 编译错误:cannot use hosts as []string数组不能自动转成切片。需要显式切片:printHosts(hosts[:])。