Go 语言:变量作用域 + 数组 + 指针
- 一、变量作用域
-
- [1.1 三大变量分类及规则](#1.1 三大变量分类及规则)
- [1.2 特殊场景:for 循环变量作用域(高频坑)](#1.2 特殊场景:for 循环变量作用域(高频坑))
-
- [写法1:循环内使用 `:=`(新局部变量)](#写法1:循环内使用
:=(新局部变量)) - 写法2:直接赋值(复用外层变量)
- [写法1:循环内使用 `:=`(新局部变量)](#写法1:循环内使用
- [1.3 变量默认零值汇总](#1.3 变量默认零值汇总)
- [1.4 变量作用域常见踩坑](#1.4 变量作用域常见踩坑)
- 二、数组
-
- [2.1 数组基础语法](#2.1 数组基础语法)
-
- [2.1.1 数组声明](#2.1.1 数组声明)
- [2.1.2 数组初始化(4 种常用方式)](#2.1.2 数组初始化(4 种常用方式))
-
- [方式1:声明 + 完整初始化](#方式1:声明 + 完整初始化)
- [方式2:短声明 `:=`(简化写法,工程常用)](#方式2:短声明
:=(简化写法,工程常用)) - 方式3:自动推断长度(`...`)
- 方式4:指定下标初始化
- [2.2 数组元素访问与遍历](#2.2 数组元素访问与遍历)
-
- [2.2.1 索引访问](#2.2.1 索引访问)
- [2.2.2 两种遍历方式](#2.2.2 两种遍历方式)
-
- [方式1:普通 for 循环(通过索引)](#方式1:普通 for 循环(通过索引))
- [方式2:for-range 遍历(Go 推荐)](#方式2:for-range 遍历(Go 推荐))
- [2.3 数组核心特性](#2.3 数组核心特性)
- [2.4 多维数组(以二维数组为例)](#2.4 多维数组(以二维数组为例))
- [2.5 数组常见踩坑](#2.5 数组常见踩坑)
- 三、指针
- 四、总结
目录
- 变量作用域(局部变量、全局变量、形式参数、代码块作用域)
- 数组(定义、初始化、访问、多维数组、核心特性)
- 指针(基础概念、指针使用、空指针、指针常见问题)
一、变量作用域
作用域 指标识符(变量、常量、函数等)在代码中可被访问的有效范围,Go 作用域以大括号 {} 代码块为划分单位,同时区分全局、局部、函数形参三大场景。
1.1 三大变量分类及规则
1.1.1 局部变量
说明
在**函数内部、普通代码块(if/for/复合花括号)**中声明的变量,仅在当前代码块内有效;代码块执行结束后,变量自动销毁。
- 函数参数、循环内变量、分支内变量都属于局部变量;
- Go 强制要求:已声明的局部变量必须被使用 ,否则编译报错
declared but not used。
基础示例
go
package main
import "fmt"
func main() {
// 函数内局部变量
var a, b int
a = 10
b = 20
c := a + b
fmt.Printf("a=%d, b=%d, c=%d\n", a, b, c)
// 独立代码块,内部变量外部无法访问
{
inner := 99
fmt.Println("代码块内变量:", inner)
}
// fmt.Println(inner) // 报错:undefined: inner
}
运行结果
a=10, b=20, c=30
代码块内变量: 99
1.1.2 全局变量
说明
在所有函数外部 声明的变量,作用域为整个当前包;
- 小写全局变量:仅本包内访问;
- 大写全局变量:可跨包导出访问;
- 全局变量允许声明但不使用,不会触发编译错误。
基础示例
go
package main
import "fmt"
// 全局变量(函数外声明)
var g int = 100
func main() {
// 函数内直接使用全局变量
fmt.Println("全局变量 g =", g)
g = 200 // 修改全局变量
fmt.Println("修改后 g =", g)
}
运行结果
全局变量 g = 100
修改后 g = 200
1.1.3 变量遮蔽(同名变量优先级)
核心规则
局部变量与全局变量可以同名 ,遵循就近原则 :当前代码块内的局部变量会遮蔽上层同名变量,优先使用局部变量,不会修改外层变量。
示例
go
package main
import "fmt"
// 全局变量
var g int = 20
func main() {
// 局部变量 g,遮蔽全局变量
var g int = 10
fmt.Println("函数内 g =", g) // 优先局部:10
}
运行结果
函数内 g = 10
1.1.4 形式参数(函数形参)
说明
函数定义时括号内的参数,本质是函数内部的局部变量,仅在当前函数内有效;函数调用时,实参会赋值给形参。
示例
go
package main
import "fmt"
// 全局变量
var a int = 20
// sum 函数的形参 a、b(属于函数局部变量)
func sum(a, b int) int {
fmt.Println("sum 函数内 a =", a) // 使用形参,遮蔽全局a
return a + b
}
func main() {
// main 局部变量 a
var a int = 10
var b int = 20
c := sum(a, b) // 实参传递给形参
fmt.Println("main 函数结果 c =", c)
}
运行结果
sum 函数内 a = 10
main 函数结果 c = 30
1.2 特殊场景:for 循环变量作用域(高频坑)
for 循环有两种写法,变量作用域完全不同,是新手最容易出错的点:
写法1:循环内使用 :=(新局部变量)
循环初始化的 a 是循环块内部局部变量,和外层变量无关。
go
package main
import "fmt"
func main() {
var a int = 0
fmt.Println("循环前 a =", a)
// for 内部 := 声明新变量,仅循环内有效
for a := 0; a < 3; a++ {
fmt.Println("循环中 a =", a)
}
fmt.Println("循环后 a =", a) // 外层变量不变:0
}
写法2:直接赋值(复用外层变量)
循环使用已有变量,循环结束后变量会保留最终值。
go
package main
import "fmt"
func main() {
var a int = 0
fmt.Println("循环前 a =", a)
// 复用外层变量 a
for a = 0; a < 3; a++ {
fmt.Println("循环中 a =", a)
}
fmt.Println("循环后 a =", a) // 循环结束,a=3
}
1.3 变量默认零值汇总
无论全局/局部变量,只声明不赋值时,系统会自动赋予零值:
| 数据类型 | 默认零值 |
|---|---|
| 整型(int/int8 等) | 0 |
| 浮点型(float32/float64) | 0.0 |
| 布尔型 | false |
| 字符串 | 空字符串 "" |
| 指针/切片/map/channel | nil |
1.4 变量作用域常见踩坑
- 局部变量声明未使用 :编译报错
declared but not used,全局变量无此限制; - 同名变量遮蔽:误以为修改局部变量会影响全局变量;
- for 循环变量混淆 :分不清
:=新建变量和赋值复用变量的区别; - 跨代码块访问变量:在花括号外部访问内部局部变量,编译报错。
二、数组
数组是长度固定、元素类型统一 的有序数据集合,内存连续分配;数组长度是类型的一部分,长度不同的数组属于不同类型,无法直接赋值转换。
2.1 数组基础语法
2.1.1 数组声明
语法格式
go
var 数组名 [数组长度] 元素类型
[长度]:必须是常量,编译期确定,运行时无法修改数组长度;- 所有元素类型必须一致。
基础示例
go
package main
import "fmt"
// 声明长度为 5 的 int 数组,默认零值 0
var arr [5]int
func main() {
fmt.Println(arr) // [0 0 0 0 0]
}
2.1.2 数组初始化(4 种常用方式)
方式1:声明 + 完整初始化
go
// 完整赋值,长度为 5
var arr1 [5]int = [5]int{1, 2, 3, 4, 5}
fmt.Println(arr1)
方式2:短声明 :=(简化写法,工程常用)
go
arr2 := [5]string{"Go", "Java", "Python"}
fmt.Println(arr2) // 未赋值元素补零值:["Go" "Java" "Python" "" ""]
方式3:自动推断长度(...)
使用 ... 替代长度,编译器根据初始化元素个数自动计算数组长度,运行后长度依然固定。
go
// 编译器推断长度为 3
arr3 := [...]int{10, 20, 30}
fmt.Println("数组长度:", len(arr3)) // 3
方式4:指定下标初始化
仅给指定索引赋值,其余元素填充零值。
go
// 下标1=2,下标3=7,其余为 0
arr4 := [5]float32{1: 2.0, 3: 7.0}
fmt.Println(arr4) // [0 2 0 7 0]
2.2 数组元素访问与遍历
2.2.1 索引访问
数组索引从 0 开始 ,数组名[索引] 读写元素;索引越界会触发运行 panic。
go
package main
import "fmt"
func main() {
arr := [3]int{10, 20, 30}
fmt.Println(arr[0]) // 第一个元素:10
arr[1] = 200 // 修改第二个元素
fmt.Println(arr) // [10 200 30]
// fmt.Println(arr[3]) // 越界,运行报错
}
2.2.2 两种遍历方式
方式1:普通 for 循环(通过索引)
go
arr := [4]int{1, 2, 3, 4}
for i := 0; i < len(arr); i++ {
fmt.Printf("索引%d,值%d\n", i, arr[i])
}
方式2:for-range 遍历(Go 推荐)
go
arr := [4]int{1, 2, 3, 4}
// idx:索引,val:元素值
for idx, val := range arr {
fmt.Printf("索引%d,值%d\n", idx, val)
}
// 忽略索引,使用空白标识符 _
for _, val := range arr {
fmt.Println(val)
}
2.3 数组核心特性
特性1:长度是类型的一部分
[5]int 和 [10]int 是两种完全不同的数据类型,无法互相赋值、传参。
go
package main
import "fmt"
func test(arr [5]int) {
fmt.Println(arr)
}
func main() {
a := [5]int{1, 2, 3, 4, 5}
b := [10]int{0, 0, 0}
test(a)
// test(b) // 报错:类型不匹配 [10]int → [5]int
}
特性2:数组是值类型
数组赋值、函数传参时,会完整拷贝一份数据,修改副本不会影响原数组。
go
package main
import "fmt"
func modify(arr [3]int) {
arr[0] = 999 // 修改副本
fmt.Println("函数内数组:", arr)
}
func main() {
origin := [3]int{1, 2, 3}
modify(origin)
fmt.Println("原数组:", origin) // 原数组不变 [1 2 3]
}
运行结果
函数内数组: [999 2 3]
原数组: [1 2 3]
2.4 多维数组(以二维数组为例)
语法格式
go
var 数组名 [行数][列数] 元素类型
示例(二维数组遍历)
go
package main
import "fmt"
func main() {
// 2行3列二维数组
twoArr := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
// 双层循环遍历
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
fmt.Printf("%d ", twoArr[i][j])
}
fmt.Println()
}
}
2.5 数组常见踩坑
- 索引越界 :访问
arr[len(arr)]等非法索引,运行直接 panic; - 长度类型不匹配:不同长度数组无法赋值、传参;
- 误以为数组可动态扩容:数组长度固定,扩容需新建数组(动态容器推荐切片);
- 传参修改原数组:数组是值类型,函数内修改副本无效,需搭配指针实现修改;
- 初始化元素超出长度 :
[3]int{1,2,3,4}编译报错。
三、指针
指针用于存储变量的内存地址 ,通过指针可以间接操作原始变量;Go 指针简化了语法,不支持指针算术运算,安全性更高。
3.1 指针基础概念
两个核心运算符
&:取地址符,放在变量前,获取变量的内存地址;*:解引用符,放在指针前,获取指针指向的变量值(也用于声明指针类型)。
3.1.1 指针声明与赋值
语法格式
go
// 声明指针变量:*类型 代表指针类型
var 指针名 *数据类型
完整使用流程(声明 → 赋值地址 → 解引用)
go
package main
import "fmt"
func main() {
var a int = 20
fmt.Printf("变量 a 的值:%d\n", a)
fmt.Printf("变量 a 的内存地址:%p\n", &a) // %p 格式化输出地址
// 1. 声明 int 类型指针
var ptr *int
// 2. 将变量 a 的地址赋值给指针
ptr = &a
fmt.Printf("指针 ptr 存储的地址:%p\n", ptr)
// 3. 解引用:通过指针获取原始值
fmt.Printf("指针指向的值:%d\n", *ptr)
// 4. 通过指针修改原始变量
*ptr = 100
fmt.Printf("修改后 a 的值:%d\n", a)
}
运行结果
变量 a 的值:20
变量 a 的内存地址:0x14000018088
指针 ptr 存储的地址:0x14000018088
指针指向的值:20
修改后 a 的值:100
3.2 空指针(nil 指针)
说明
指针声明后未赋值 时,默认零值为 nil(空指针),表示该指针不指向任何有效内存地址。
- 判断空指针:
指针变量 == nil; - 禁止直接解引用空指针 ,会触发运行
panic崩溃。
安全使用示例
go
package main
import "fmt"
func main() {
// 声明指针,未赋值 → 默认 nil
var ptr *int
fmt.Println("指针是否为空:", ptr == nil) // true
// 危险写法:*ptr 解引用空指针 → panic
// fmt.Println(*ptr)
// 安全写法:先判空,再解引用
if ptr != nil {
fmt.Println(*ptr)
} else {
fmt.Println("空指针,无法取值")
}
}
3.3 指针结合函数(修改原始变量)
数组、普通变量是值类型,函数传参为拷贝;使用指针传参可以直接修改原始变量,是指针最常用场景。
示例:指针传参修改变量
go
package main
import "fmt"
// 接收 int 指针类型
func changeNum(num *int) {
*num = 888 // 解引用,修改原始变量
}
func main() {
a := 10
changeNum(&a) // 传递变量地址
fmt.Println(a) // 888(原始变量被修改)
}
3.4 多级指针
指向指针的指针,日常开发使用较少,语法规则一致:
go
package main
import "fmt"
func main() {
a := 10
var p1 *int = &a // 一级指针
var p2 **int = &p1 // 二级指针
fmt.Println(*p1) // 10
fmt.Println(**p2) // 10
}
3.5 指针核心规则与踩坑
- 禁止指针算术运算 :Go 不支持
ptr++、ptr + 1等指针偏移操作,避免内存越界; - 空指针必判空 :任何解引用操作前,必须判断
ptr != nil,否则程序崩溃; - 类型匹配 :指针类型必须和变量类型一致,
*int只能接收int变量地址; - 不要返回局部变量指针:局部变量函数结束后销毁,返回其指针会指向无效内存;
&和*配对使用 :取地址用&,取值用*,不要混用。
四、总结
| 模块 | 核心要点 |
|---|---|
| 变量作用域 | 代码块划分作用域;局部优先遮蔽全局;局部变量必须使用;for 循环变量区分 := 和赋值 |
| 数组 | 长度固定、长度属于类型;值类型(传参拷贝);索引从 0 开始,越界报错;... 自动推断长度 |
| 指针 | & 取地址、* 解引用;默认 nil,解引用前必判空;用于函数修改原始变量;无指针运算 |