Go 语言基础进阶:指针、init、匿名函数/闭包、defer

最近我在学 Go 的时候,发现这些内容特别容易"看懂了但写不顺":指针 怎么用才合理?new 和 make 到底差在哪?init 什么时候会跑?匿名函数/闭包 写并发时怎么不翻车?以及 defer 为什么有时候"看起来没问题却很危险"。这篇我将总结一下go语言里遇到的的常见问题。


目录

[1. 指针:基本概念与核心操作](#1. 指针:基本概念与核心操作)

[2. Go 指针的特性:和 C/C++ 最大差异](#2. Go 指针的特性:和 C/C++ 最大差异)

[3. new vs make:内存分配别再混了(表格对比)](#3. new vs make:内存分配别再混了(表格对比))

[4. 指针的典型使用场景:怎么用才划算](#4. 指针的典型使用场景:怎么用才划算)

[5. init:包的自动初始化函数(顺序很关键)](#5. init:包的自动初始化函数(顺序很关键))

[6. 匿名函数与闭包:写得灵活,也最容易踩坑](#6. 匿名函数与闭包:写得灵活,也最容易踩坑)

[7. defer:延迟调用的规则、用法、陷阱](#7. defer:延迟调用的规则、用法、陷阱)

[8. 总结](#8. 总结)


1. 指针:基本概念与核心操作

1.1 什么是指针

指针就是"存放变量内存地址的变量"。

它的价值在于:可以间接访问/修改原变量,避免复制一份新数据。

1.2 两个核心操作符:&*

  • &x:取地址,得到"指向 x 的指针"

  • *p:解引用,通过指针访问/修改它指向的值

    package main

    import "fmt"

    func main() {
    x := 10
    p := &x // p 的类型是 *int
    fmt.Println(x) // 10

    复制代码
      *p = 20        // 通过指针修改 x
      fmt.Println(x) // 20

    }

1.3 指针类型与声明

  • *T 表示指向 T 的指针类型,比如 *int*MyStruct

  • 指针的零值是 nil(很重要,后面会讲它的风险)

    var p *int // 默认是 nil


2. Go 指针的特性:和 C/C++ 最大差异

2.1 禁止指针运算(Go 直接封死这个坑)

Go 不支持 p++p + 4 这种操作。

好处很明显:少了很多指针越界、乱指内存的事故。

2.2 强类型安全:指针类型不能乱来

*int*float64 不能互相赋值。

就算是别名类型也一样:

复制代码
type MyInt int

var a int = 1
var p1 *int = &a

var b MyInt = 2
var p2 *MyInt = &b

// p1 = p2 // 编译不通过:类型不同

真要"硬转",就得用 unsafe,但基本不用。

2.3 nil 指针:解引用直接 panic

复制代码
var p *int
// fmt.Println(*p) // 直接 panic
if p != nil {
    fmt.Println(*p)
}

2.4 结构体指针的语法糖:不用 ->

Go 里结构体指针访问字段仍然用 .,编译器会自动解引用:

复制代码
type User struct {
    Name string
}

func main() {
    u := &User{Name: "Tom"}
    fmt.Println(u.Name) // 不需要写 (*u).Name
}

3. new vs make:内存分配别再混了

Go 里最常见的一个"迷惑点":new 和 make 都像是在创建东西,但它们完全不是一类。

对比项 new(T) make(T, ...)
返回值 *T(指针) T(不是指针)
是否初始化内部结构 只做零值初始化 会初始化底层结构
适用类型 任意类型 仅:slice / map / chan
典型用途 拿到一个可用的对象地址 创建可直接使用的引用类型

3.1 new:给你一块"零值内存",并返回指针

复制代码
p := new(int) // p 是 *int,且 *p == 0
*p = 5

3.2 make:专门给引用类型"搭好底层结构"

下面这个就是经典踩坑:

复制代码
var m map[string]int
// m["a"] = 1 // panic:assignment to entry in nil map

m = make(map[string]int)
m["a"] = 1 // OK

4. 指针的典型使用场景:怎么用才划算

场景 1:让函数修改外部变量

Go 参数默认是值传递(会拷贝一份)。想改外面的值,就传指针:

复制代码
func addOne(x *int) {
    *x += 1
}

func main() {
    a := 10
    addOne(&a)
    // a == 11
}

场景 2:避免大结构体/大数组拷贝

如果结构体很大,值传递会拷贝一大坨,传指针更省:

复制代码
type Big struct {
    Data [100000]int
}

func process(b *Big) {
    b.Data[0] = 1
}

场景 3:共享同一份数据

多个地方都要"看同一份、改了大家都能看到",用指针更直观。


5. init:包的自动初始化函数(顺序很关键)

init 是 Go 的特殊函数:程序启动时自动执行,不能手动调用。

5.1 核心特性

  • 固定签名:func init()

  • main 之前执行

  • 同一个包可以有多个 init

5.2 执行顺序

整体顺序可以记成:

  1. 先初始化依赖包(被依赖的先初始化)

  2. 再初始化当前包的全局变量(按声明顺序)

  3. 最后执行 init(同包内多个 init 按出现顺序执行)

Tip:不要写"依赖 init 执行顺序"的代码,可读性和稳定性都很差。

5.3 常见用途

  • 全局变量需要复杂初始化逻辑

  • 驱动/组件注册(很多库就是靠 init 自动注册)

  • 启动前做环境检查、预计算等


6. 匿名函数与闭包:写得灵活,也最容易踩坑

6.1 匿名函数 3 种常见用法

(1)立即执行

复制代码
func main() {
    func() {
        println("run now")
    }()
}

(2)赋值给变量

复制代码
f := func(x int) int { return x * 2 }
println(f(3)) // 6

(3)作为参数/返回值

复制代码
func apply(x int, fn func(int) int) int {
    return fn(x)
}

6.2 闭包是什么?(重点)

闭包可以"捕获外部变量",形成自己的执行环境。

它捕获的是变量本身(引用),不是当时的值。

复制代码
func counter() func() int {
    x := 0
    return func() int {
        x++
        return x
    }
}

func main() {
    c := counter()
    println(c()) // 1
    println(c()) // 2
}

6.3 经典踩坑:for 循环变量捕获

很多人第一次写 goroutine 会这样:

复制代码
for i := 0; i < 3; i++ {
    go func() {
        println(i)
    }()
}

问题:闭包捕获的是同一个 i,循环结束 i 变成 3,可能打印一堆 3。

✅ 正确写法(把 i "拷贝一份"):

复制代码
for i := 0; i < 3; i++ {
    i := i // 关键:创建新变量
    go func() {
        println(i)
    }()
}

Tip:看到 "for + goroutine/defer + 匿名函数",我现在都会条件反射地检查循环变量。

6.4 闭包的另一个坑:可能导致"变量活太久"

闭包长期被持有(比如挂到全局、长生命周期 channel 里),会让它捕获的外部变量也一直无法回收。

写缓存/回调系统时要特别注意。


7. defer:延迟调用的规则、用法、陷阱

defer 会把函数调用延迟到当前函数结束前执行(正常 return / panic 都会走)。

7.1 两条必背规则

规则 1:后进先出(LIFO)

复制代码
func main() {
    defer println("A")
    defer println("B")
    // 输出顺序:B 再 A
}

规则 2:参数会立即求值

复制代码
func main() {
    x := 1
    defer fmt.Println(x) // 这里已经把 x 的值算好了
    x = 2
    // 输出 1
}

7.2 常见使用场景

  • 资源清理:文件/连接/锁

    f, _ := os.Open("a.txt")
    defer f.Close()

  • panic 恢复:配合 recover()

    defer func() {
    if r := recover(); r != nil {
    fmt.Println("recover:", r)
    }
    }()

  • 修改有名返回值:defer 可以在返回前"最后再改一次"

    func test() (res int) {
    defer func() { res++ }()
    return 10 // 最终返回 11
    }

7.3 defer 的常见坑

坑 1:不要在循环里乱 defer

循环里 defer 会"攒到最后才执行",资源可能长时间不释放。

更稳的写法:用匿名函数包一层,让 defer 每次循环都能及时执行

复制代码
for i := 0; i < 3; i++ {
    func() {
        f, _ := os.Open("a.txt")
        defer f.Close()
        // use f
    }()
}

坑 2:defer 一个 nil 函数值会 panic

复制代码
var f func()
defer f() // 运行时 panic

8. 总结

  • 指针:保留"能改原值/省拷贝"的能力,但 Go 用强类型 + 禁止指针运算来限制风险

  • new vs make:new 给"零值指针",make 给"可用的引用类型底座"

  • init:自动执行,顺序是"依赖包 → 全局变量 → init",别依赖跨文件顺序

  • 匿名函数/闭包:灵活但容易踩 for 循环捕获坑,并发里一定要小心

  • defer:LIFO + 参数即时求值,资源清理很香,但循环里别乱用

如果你也在学 Go,建议把本文的循环变量捕获new/make 的区别当成必过关卡:这俩是最容易"看起来会了但实战翻车"的点。

相关推荐
Scott9999HH7 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社8 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海8 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖9 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大11 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai12 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜12 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa12 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio