一、数组:最固执的数据容器
如果你曾经被Go语言中切片(slice)的灵活性惯坏,那么遇见数组(array)时,可能会觉得它像个从上古时代穿越而来的老古董。这个固执的家伙有个核心特质:长度一旦声明,至死不变。
想象一下,数组就像高度强迫症的收纳师整理好的药盒——每个格子都有固定位置,周一放维生素,周二放钙片,你想临时塞个周三的鱼油?对不起,没门!
但别急着划走!正是这种“固执”,让数组在特定场景下成为性能王者。当你知道元素数量固定且需要内存连续时,数组比切片少了动态扩容的开销,访问速度直接起飞。
二、声明数组的四种花式姿势
姿势1:标准声明法——最正式的自我介绍
package main
import "fmt"
func main() {
// 完整声明格式:var 数组名 [长度]类型
var classA [3]string
// 逐个填装数据
classA[0] = "小明"
classA[1] = "小红"
classA[2] = "小刚"
fmt.Printf("班级A成员:%v\n", classA)
fmt.Printf("数组长度:%d\n", len(classA))
}
运行结果:
班级A成员:[小明 小红 小刚]
数组长度:3
关键点:
- 长度
[3]是类型的一部分,[3]string和[4]string是两种完全不同的类型 - 初始化为类型零值(字符串是"",整型是0,布尔是false)
- 索引从0开始,最大到
长度-1
姿势2:初始化声明法——出生即丰满
func main() {
// 声明同时初始化,让数组“出生即巅峰”
scores := [5]int{98, 85, 92, 67, 88}
// 也可以只初始化部分位置,其余自动补零值
partialScores := [5]int{90, 85} // [90 85 0 0 0]
fmt.Printf("全量分数:%v\n", scores)
fmt.Printf("部分分数:%v\n", partialScores)
}
姿势3:编译器数数法——偷懒者的福音
func main() {
// 让编译器帮你数元素个数,用...代替具体数字
weekdays := [...]string{"周一", "周二", "周三", "周四", "周五"}
fmt.Printf("工作日:%v\n", weekdays)
fmt.Printf("工作日数量:%d\n", len(weekdays))
}
运行结果:
工作日:[周一 周二 周三 周四 周五]
工作日数量:5
这是最常用的写法之一,既保证安全又不用掰着手指数个数,完美!
姿势4:指定索引初始化法——精准投放
func main() {
// 只给特定位置赋值,像玩填格子游戏
taskStatus := [5]string{0: "已完成", 3: "进行中", 4: "待开始"}
fmt.Printf("任务状态:%v\n", taskStatus)
}
运行结果:
任务状态:[已完成 待开始 进行中]
空字符串位置表示那些没被初始化的索引,自动获得了零值。
三、数组的内心世界:值类型的倔强
这是数组最容易被误解的特性!数组是值类型,不是引用类型。这意味着:
func main() {
original := [3]int{10, 20, 30}
copy := original // 这里是完整复制,不是传递引用!
copy[0] = 999 // 修改副本
fmt.Printf("原数组:%v\n", original)
fmt.Printf副本数组:%v\n", copy)
}
运行结果:
原数组:[10 20 30]
副本数组:[999 20 30]
重要结论:数组赋值或传参时会发生整个数组的复制,内存开销与数组长度成正比。大数组频繁传参?性能杀手无疑!
四、遍历数组:两种检索姿势
传统for循环:掌控感满分
func main() {
prices := [4]float64{28.5, 36.0, 42.8, 19.9}
// 方式1:标准for循环,完全掌控索引
for i := 0; i < len(prices); i++ {
fmt.Printf("商品%d价格:%.1f元\n", i+1, prices[i])
}
}
range遍历:优雅的代名词
func main() {
fruits := [3]string{"苹果", "香蕉", "橙子"}
// 方式2:range遍历,同时获取索引和值
for index, value := range fruits {
fmt.Printf("水果筐%d号:%s\n", index, value)
}
// 如果只需要值,用下划线忽略索引
for _, fruit := range fruits {
fmt.Printf("今天吃:%s\n", fruit)
}
}
五、多维数组:数据俄罗斯套娃
当一维数组不够用,试试多维的:
func main() {
// 声明一个3x3的井字棋棋盘
var ticTacToe [3][3]string
// 初始化棋盘
ticTacToe = [3][3]string{
{" ", "X", "O"},
{"O", "X", " "},
{"X", " ", "O"},
}
fmt.Println("井字棋当前局势:")
for i := 0; i < len(ticTacToe); i++ {
fmt.Println(ticTacToe[i])
}
// 更直观的初始化方式
matrix := [2][2]int{
{1, 2},
{3, 4}, // 注意:最后一行的逗号不能少
}
fmt.Printf("二维矩阵:%v\n", matrix)
}
六、数组实战:成绩分析系统
来点真刀真枪的实战,下面是一个完整的成绩分析程序:
package main
import (
"fmt"
"math"
)
func main() {
// 初始化学生成绩数组
scores := [6]float64{85.5, 92.0, 78.5, 96.5, 88.0, 72.5}
// 计算平均分
total := 0.0
for _, score := range scores {
total += score
}
average := total / float64(len(scores))
// 计算最高分和最低分
maxScore := scores[0]
minScore := scores[0]
for i := 1; i < len(scores); i++ {
if scores[i] > maxScore {
maxScore = scores[i]
}
if scores[i] < minScore {
minScore = scores[i]
}
}
// 计算标准差
variance := 0.0
for _, score := range scores {
variance += math.Pow(score-average, 2)
}
stdDev := math.Sqrt(variance / float64(len(scores)))
fmt.Printf("成绩统计报告:\n")
fmt.Printf("成绩列表:%v\n", scores)
fmt.Printf("平均分:%.2f\n", average)
fmt.Printf("最高分:%.2f\n", maxScore)
fmt.Printf("最低分:%.2f\n", minScore)
fmt.Printf("标准差:%.2f\n", stdDev)
// 成绩分级统计
gradeCount := [5]int{} // A,B,C,D,E等级
for _, score := range scores {
switch {
case score >= 90:
gradeCount[0]++ // A
case score >= 80:
gradeCount[1]++ // B
case score >= 70:
gradeCount[2]++ // C
case score >= 60:
gradeCount[3]++ // D
default:
gradeCount[4]++ // E
}
}
grades := [5]string{"A", "B", "C", "D", "E"}
for i, count := range gradeCount {
fmt.Printf("%s等级人数:%d\n", grades[i], count)
}
}
七、数组的优缺点:什么场合该用它?
优点:
- 内存连续:CPU缓存友好,访问速度快
- 编译期确定:类型安全,边界检查
- 简单可控:没有动态内存分配 overhead
致命缺点:
- 长度固定:无法适应动态数据需求
- 值复制:大数组传参效率低下
- 不够灵活:缺少现代容器的便捷方法
使用场景建议:
- 固定长度的配置项(如RGB颜色值[3]int)
- 系统启动参数(如命令行参数)
- 数学计算中的固定维度向量/矩阵
- 协议头、数据包格式等固定结构
八、数组vs切片:别傻傻分不清
很多Go新手会混淆数组和切片,记住这个核心区别:
func main() {
// 数组:长度是类型的一部分
array := [3]int{1, 2, 3}
// 切片:动态长度,底层引用数组
slice := []int{1, 2, 3}
fmt.Printf("数组类型:%T\n", array) // [3]int
fmt.Printf("切片类型:%T\n", slice) // []int
// 数组传参:完整复制
modifyArray(array)
fmt.Printf("修改后原数组:%v\n", array) // 不变!
// 切片传参:传递引用
modifySlice(slice)
fmt.Printf("修改后原切片:%v\n", slice) // 变了!
}
func modifyArray(arr [3]int) {
arr[0] = 999
}
func modifySlice(sli []int) {
sli[0] = 999
}
九、高级玩法:数组内存布局探秘
理解数组的内存布局,能帮你写出更高效的代码:
package main
import (
"fmt"
"unsafe"
)
func main() {
// 查看数组内存占用
intArray := [5]int32{1, 2, 3, 4, 5}
stringArray := [3]string{"Go", "数组", "教程"}
fmt.Printf("int数组大小:%d 字节\n", unsafe.Sizeof(intArray))
fmt.Printf("string数组大小:%d 字节\n", unsafe.Sizeof(stringArray))
// 验证内存连续性
fmt.Printf("元素内存地址:\n")
for i := 0; i < len(intArray); i++ {
fmt.Printf("intArray[%d]地址:%p\n", i, &intArray[i])
}
}
运行这个程序,你会发现数组元素在内存中绝对是连续存放的,这也是数组访问速度快的根本原因。
结语:数组虽老,功力犹在
学完这篇深度解析,你应该不会再小看Go数组这个"老古董"了。虽然在实际开发中切片出场率更高,但数组作为切片的基础底层,理解它对于掌握Go语言内存模型至关重要。
记住数组的座右铭:我的地盘大小固定,但性能极致。在合适的场景使用数组,让你的程序既快又稳。
下次遇到固定长度的数据集合,不妨给数组一个机会——这个看似简单的数据结构,可能会用性能给你带来惊喜!
完整代码获取:文中所有示例均已测试通过,建议亲手敲一遍加深理解。数组就像编程世界的基本功,看似简单,练到极致就是高手!

被折叠的 条评论
为什么被折叠?



