Go学习第5天:变量作用域 + 数组 + 指针

Go 语言:变量作用域 + 数组 + 指针

目录

  1. 变量作用域(局部变量、全局变量、形式参数、代码块作用域)
  2. 数组(定义、初始化、访问、多维数组、核心特性)
  3. 指针(基础概念、指针使用、空指针、指针常见问题)

一、变量作用域

作用域 指标识符(变量、常量、函数等)在代码中可被访问的有效范围,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 变量作用域常见踩坑

  1. 局部变量声明未使用 :编译报错 declared but not used,全局变量无此限制;
  2. 同名变量遮蔽:误以为修改局部变量会影响全局变量;
  3. for 循环变量混淆 :分不清 := 新建变量和赋值复用变量的区别;
  4. 跨代码块访问变量:在花括号外部访问内部局部变量,编译报错。

二、数组

数组是长度固定、元素类型统一 的有序数据集合,内存连续分配;数组长度是类型的一部分,长度不同的数组属于不同类型,无法直接赋值转换。

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 数组常见踩坑

  1. 索引越界 :访问 arr[len(arr)] 等非法索引,运行直接 panic;
  2. 长度类型不匹配:不同长度数组无法赋值、传参;
  3. 误以为数组可动态扩容:数组长度固定,扩容需新建数组(动态容器推荐切片);
  4. 传参修改原数组:数组是值类型,函数内修改副本无效,需搭配指针实现修改;
  5. 初始化元素超出长度[3]int{1,2,3,4} 编译报错。

三、指针

指针用于存储变量的内存地址 ,通过指针可以间接操作原始变量;Go 指针简化了语法,不支持指针算术运算,安全性更高。

3.1 指针基础概念

两个核心运算符

  1. &取地址符,放在变量前,获取变量的内存地址;
  2. *解引用符,放在指针前,获取指针指向的变量值(也用于声明指针类型)。

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 指针核心规则与踩坑

  1. 禁止指针算术运算 :Go 不支持 ptr++ptr + 1 等指针偏移操作,避免内存越界;
  2. 空指针必判空 :任何解引用操作前,必须判断 ptr != nil,否则程序崩溃;
  3. 类型匹配 :指针类型必须和变量类型一致,*int 只能接收 int 变量地址;
  4. 不要返回局部变量指针:局部变量函数结束后销毁,返回其指针会指向无效内存;
  5. &* 配对使用 :取地址用 &,取值用 *,不要混用。

四、总结

模块 核心要点
变量作用域 代码块划分作用域;局部优先遮蔽全局;局部变量必须使用;for 循环变量区分 := 和赋值
数组 长度固定、长度属于类型;值类型(传参拷贝);索引从 0 开始,越界报错;... 自动推断长度
指针 & 取地址、* 解引用;默认 nil,解引用前必判空;用于函数修改原始变量;无指针运算
相关推荐
Sam_Deep_Thinking1 小时前
java中的class到底是个什么东西?
java·开发语言·面试
资深流水灯工程师1 小时前
PySide6 QMainWindow与QWidget秒解
开发语言·python
MartinYeung52 小时前
[论文学习]针对 LLM 的间接提示注入攻击用于高效隐私洩露之深度分析
人工智能·学习
字节高级特工2 小时前
智能指针原理与使用场景全解析
开发语言·c++·算法
码界索隆2 小时前
Python转Java系列:面向对象基础
java·开发语言·python
逻辑星辰2 小时前
x-ds-pow-response逆向分析
开发语言·人工智能·python·深度学习·算法
AI棒棒牛2 小时前
YOLO26 全网独家改进创新: MIT 2025 振荡状态空间模型:引入可学习的阻尼机制,独家创新!
人工智能·学习·目标检测·计算机视觉·yolo26
Lewiis2 小时前
白话桶排序
数据结构·算法·golang·排序算法
留白_2 小时前
pandas进阶学习
学习·pandas