数组定义
go
var 数组变量名 [元素数量]T
数组变量名:自定义的变量名称,遵循 Go 的变量命名规则。元素数量:数组的长度,必须是常量表达式(编译时确定)。T:数组元素的类型,可以是任何有效的 Go 数据类型(如int、string、float64等)。
go
var numbers [5]int
//定义一个长度为 5 的整型数组
go
var fruits [3]string = [3]string{"apple", "banana", "orange"}
定义一个长度为 3 的字符串数组并初始化
数组的初始化
go
var arr [3]int // 初始值为 [0, 0, 0]
未显式初始化
go
var arr = [3]int{1, 2, 3} // 初始值为 [1, 2, 3]
显式初始化
go
var arr = [...]int{1, 2, 3} // 长度自动推断为 3
省略长度初始化
数组的访问和修改
数组的遍历
go
var a = [...]string{"北京", "上海", "深圳"}
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
go
for index, value := range a {
fmt.Println(index, value)
}
两种方法各有优势:
- for循环适合需要复杂循环逻辑的场景
- range语法更简洁,是Go语言推荐的遍历方式
- range会复制值到临时变量,对大型数组需注意性能影响
多维数组
go
var matrix [3][3]int // 定义一个3x3的二维整数数组
go
matrix := [3][3]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
go
for i := 0; i < len(matrix); i++ {
for j := 0; j < len(matrix[i]); j++ {
fmt.Printf("matrix[%d][%d] = %d\n", i, j, matrix[i][j])
}
}
动态扩容的二维数据结构
go
dynamicMatrix := make([][]int, 3)
for i := range dynamicMatrix {
dynamicMatrix[i] = make([]int, 3)
}
为什么必须用 for 循环?
当执行 dynamicMatrix := make([][]int, 3) 时,Go 只是在内存中分配了一个长度为 3 的外层切片,而它里面的每一个元素(类型是 []int )的默认零值都是 nil 。
如果省略了 for 循环,直接执行 dynamicMatrix[0][0] = 1 ,程序会直接引发 panic: index out of range (对 nil 切片进行索引赋值)。因此,遍历并初始化每一行是必不可少的步骤。
数组是值类型
数组作为值类型意味着数组变量直接存储数组的数据本身,而非引用。当数组被赋值给另一个变量或作为参数传递时,会创建一个完整的副本。对副本的修改不会影响原始数组。
go
package main
import "fmt"
func modifyArray(arr [3]int) {
arr[0] = 100
fmt.Println("Inside modifyArray:", arr)
}
func main() {
original := [3]int{1, 2, 3}
copy := original
copy[1] = 20
fmt.Println("Original array:", original)
fmt.Println("Copy array:", copy)
modifyArray(original)
fmt.Println("After modifyArray:", original)
}
inform7
Original array: [1 2 3]
Copy array: [1 20 3]
Inside modifyArray: [100 2 3]
After modifyArray: [1 2 3]
与引用类型的对比
切片(slice)、映射(map)等引用类型不同,引用类型的变量赋值和传参仅复制引用(指针),因此修改副本会影响原始数据。例如:
go
slice := []int{1, 2, 3}
sliceCopy := slice
sliceCopy[0] = 100
fmt.Println(slice) // 输出 [100 2 3]
性能注意事项
由于值类型的数组会完整复制数据,大型数组的频繁赋值或传参可能导致性能问题。此时可考虑使用指针或切片来避免复制:
go
func modifyArrayByPointer(arr *[3]int) {
arr[0] = 100
}
func main() {
arr := [3]int{1, 2, 3}
modifyArrayByPointer(&arr)
fmt.Println(arr) // 输出 [100 2 3]
}
适用场景
值类型数组适合以下场景:
- 需要确保数据不被意外修改。
- 数组规模较小,复制开销可忽略。
- 需要线程安全(副本独立)。
如何真正在函数中修改原来的数据
1、传递数组的指针(如果必须用固定长度的数组)
go
package main
import "fmt"
// 参数变成指针类型:*[3]int
func modifyArrayWithPointer(x *[3]int) {
// Go 有语法糖,虽然 x 是指针,但可以直接用 x[0],不需要写成 (*x)[0]
x[0] = 100
}
func main() {
a := [3]int{10, 20, 30}
modifyArrayWithPointer(&a) // 注意这里加了 &,传递地址
fmt.Println(a) // 输出: [100 20 30] (原数组被修改了!)
}
写法**"三大黄金法则"**
法则一:声明用 *,调用用 &(固定搭配)
当你希望在函数里修改外面的变量时,函数定义和函数调用必须像"锁和钥匙"一样配套使用:
- 函数定义(造锁) :参数类型前面必须加 * ,表示"我需要一个内存地址"。
-
func foo(x *[3]int)
-
func bar(n *int)
- 函数调用(开锁) :传参时变量前面必须加 & ,表示"我要把我的内存地址交给你"。
-
a := [3]int{...} ➡️ foo(&a)
-
b := 10 ➡️ bar(&b)
法则二:数组和结构体的"自动解引用"(Go 特有的固定语法糖)
C 或 C++ 等老牌语言中,如果 x 是一个指针,你必须先用 * 把真实数据"取"出来,然后再操作,比如写成 (*x)[0] = 100 。这非常繁琐。
Go 语言在这里做了一个固定的"语法糖"规定: 只要你是 数组指针 或者 结构体指针 ,你 不需要 手动写 (*x) ,直接像操作普通变量一样操作它,Go 编译器在底层会自动帮你转换!
go
func modify(arr *[3]int) {
arr[0] = 100 // ✅ 标准写法,极其常见
// (*arr)[0] = 100 // 也可以这么写,但没人会这么干,太啰嗦了
}
法则三:基础类型的指针,必须手动写 * (没有语法糖)
刚才说数组和结构体可以"偷懒",但如果你传的是基础类型(比如 int , string , bool )的指针,就 没有 语法糖了,必须老老实实写 * 。
为什么?因为基础类型没有像 [0] 这样的索引,也没有像 .Name 这样的字段,编译器无法自动推断。
go
package main
import "fmt"
func modifyInt(n *int) {
// n = 100 // ❌ 报错!n 存的是个内存地址(比如 0xc00001a0a8),不能把 100 赋给地址
*n = 100 // ✅ 固定写法:必须加上 *,意思是"顺着地址找到那个坑位,把 100 放进去"
}
func main() {
x := 10
modifyInt(&x) // 传入地址
fmt.Println(x) // 输出 100
}
2、使用切片 Slice(🌟 推荐做法,Go 的灵魂)
在实际的 Go 开发中,我们极少直接传递数组,而是 几乎总是传递切片(Slice) 。
切片的底层自带了指向数组的指针,所以传递切片时,只会复制切片的"头部信息"(很轻量,只有 24 个字节),而底层的真实数据是共享的。
go
package main
import "fmt"
// 参数变成切片类型:[]int (注意中括号里没有数字了)
func modifySlice(x []int) {
x[0] = 100
}
func main() {
// 1. 如果一开始定义的就是切片
s := []int{10, 20, 30} // 没写长度,所以这是切片
modifySlice(s)
fmt.Println(s) // 输出: [100 20 30]
// 2. 如果一开始定义的是数组,可以切一刀传进去
a := [3]int{10, 20, 30}
modifySlice(a[:]) // a[:] 把数组转换成了切片传进去
fmt.Println(a) // 输出: [100 20 30]
}
切片语法,拆解为 四个必须掌握的固定套路 :
套路一:如何声明一个切片(和数组的唯一区别)
在 Go 里,数组和切片长得像双胞胎,区别只在 中括号里有没有数字 :
- 数组(固定长度) :中括号里 有 数字(或 ... )。
go
a := [3]int{10, 20, 30} // 明确写了长度 3,是数组
b := [...]int{10, 20, 30} // 让编译器数,结果还是 [3]int,是数组
套路二:如何把数组变成切片("切一刀"语法 [:])
如果你手头刚好有一个数组(比如 a := [3]int{10, 20, 30} ),但你要调用的函数 modifySlice 只接收切片 []int 。怎么把数组传进去?
这就是 Go 语言最经典的 切片操作符 [start:end] 。
-
标准语法 : array[起始索引 : 结束索引] (左闭右开,即包含 start,不包含 end)。
-
偷懒语法(常用) :
-
a[0:3] :取索引 0, 1, 2(等价于完整的 a)。
-
a[:2] :不写 start,默认从 0 开始。取前 2 个元素。
-
a[1:] :不写 end,默认一直取到最后。
-
a[:] :两头都不写,代表"从头取到尾"!
这就是为什么代码里写 modifySlice(a[:]) 。它把整个数组包装成了一个切片,传给了函数。
套路三:切片的"暗箱操作"(为什么能修改原数据?)
切片之所以神奇,是因为它的底层结构。你可以把切片想象成一个**"带指针的结构体"**。
当执行 s := []int{10, 20, 30} 时,Go 做了两件事:
-
在内存深处悄悄建了一个 隐形的数组 [10, 20, 30] 。
-
创建了一个"切片头(Slice Header)",它包含 3 个信息:
-
Data :一个 指针 ,指向那个隐形数组的第一个元素。
-
Len :长度(目前装了几个元素,这里是 3)。
-
Cap :容量(底层数组最大能装几个元素,这里也是 3)。
核心机制 :
当你把切片传给函数 func modifySlice(x []int) 时,Go 依然是"按值传递",但它 只复制了那个包含指针的"切片头" 。
因为"切片头"里的指针指向同一个底层数组,所以你在函数里 x[0] = 100 ,外面的 s 也会看到变化。
套路四:切片的"致命陷阱"(面试必考,日常必踩)
虽然切片传递很方便,但有一个 极其危险的陷阱: append 操作可能会让原数据失效!
go
package main
import "fmt"
func modifySliceAndAppend(x []int) {
// 1. 修改现有元素(外面会跟着变,因为指针指向同一个地方)
x[0] = 100
// 2. 危险操作:尝试增加新元素
x = append(x, 40) // 此时 x 变成了 [100 20 30 40]
}
func main() {
s := []int{10, 20, 30}
modifySliceAndAppend(s)
fmt.Println(s) // 结果是什么?
}
答案是: [100, 20, 30] ! 那个 40 并没有加进去!
为什么? 因为你在函数里 append 时,底层的隐形数组装不下了(原来只能装 3 个),Go 会自动去内存里找一块 更大的新地方 ,把老数据搬过去,再加上 40 。
此时,函数里的 x 已经指向了 新数组 ,而外面的 s 依然傻傻地指向 老数组 。两者彻底断绝了关系!
如何解决?(标准写法) 如果你要在函数里 append 元素,并且希望外面能拿到结果, 函数必须把修改后的切片 return 回来
go
// 必须有返回值 []int
func modifySliceAndAppendSafe(x []int) []int {
x[0] = 100
x = append(x, 40)
return x // 乖乖还回去
}
func main() {
s := []int{10, 20, 30}
// 必须用变量接收返回值,覆盖原来的 s
s = modifySliceAndAppendSafe(s)
fmt.Println(s) // 成功输出: [100 20 30 40]
}
总结口诀:
-
改元素,直接传切片。
-
加元素,必须带返回。