Go语言基础之数组

数组定义

go 复制代码
var 数组变量名 [元素数量]T
 
  • 数组变量名:自定义的变量名称,遵循 Go 的变量命名规则。
  • 元素数量:数组的长度,必须是常量表达式(编译时确定)。
  • T:数组元素的类型,可以是任何有效的 Go 数据类型(如 intstringfloat64 等)。
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] (原数组被修改了!)
}
 

写法**"三大黄金法则"**

法则一:声明用 *,调用用 &(固定搭配)

当你希望在函数里修改外面的变量时,函数定义和函数调用必须像"锁和钥匙"一样配套使用:

  1. 函数定义(造锁) :参数类型前面必须加 * ,表示"我需要一个内存地址"。
  • func foo(x *[3]int)

  • func bar(n *int)

  1. 函数调用(开锁) :传参时变量前面必须加 & ,表示"我要把我的内存地址交给你"。
  • 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 做了两件事:

  1. 在内存深处悄悄建了一个 隐形的数组 [10, 20, 30] 。

  2. 创建了一个"切片头(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]
}

总结口诀:

  • 改元素,直接传切片。

  • 加元素,必须带返回。

相关推荐
liurunlin8883 小时前
Go环境搭建(vscode调试)
开发语言·vscode·golang
luom01023 小时前
SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现
java·spring boot·后端
黄俊懿3 小时前
【架构师从入门到进阶】第二章:系统衡量指标——第一节:伸缩性、扩展性、安全性
分布式·后端·中间件·架构·系统架构·架构设计
希望永不加班4 小时前
SpringBoot 核心配置文件:application.yml 与 application.properties
java·spring boot·后端·spring
散峰而望4 小时前
【基础算法】从入门到实战:递归型枚举与回溯剪枝,暴力搜索的初级优化指南
数据结构·c++·后端·算法·机器学习·github·剪枝
前端付豪4 小时前
Memory V1:让 AI 记住你的关键信息
前端·后端·llm
编码忘我5 小时前
RokcetMq的顺序消费、防丢失、去重
后端
毕设源码-朱学姐5 小时前
【开题答辩全过程】以 基于SpringBoot+Vue的百货商品进出货平台为例,包含答辩的问题和答案
java·spring boot·后端
码路飞5 小时前
Claude Code 大规模封号,我花了一晚上才搞明白:setup token 和 API key 根本不是一回事
后端·claude