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 的区别当成必过关卡:这俩是最容易"看起来会了但实战翻车"的点。

相关推荐
XMYX-01 小时前
28 - Go JSON 数据操作
开发语言·golang·json
三*一1 小时前
Mapbox GL JS 自研面要素整形工具开发实录
开发语言·javascript·arcgis·ecmascript
超级小星星2 小时前
C 语言结构体内存对齐深度解析:从概念到实战
c语言·开发语言
狮子座明仔2 小时前
AgentSPEX:当 Agent 框架开始把“控制流“从 Python 里抠出来
开发语言·python
笨笨饿2 小时前
74_SysTick滴答定时器中断
c语言·开发语言·人工智能·单片机·嵌入式硬件·算法·学习方法
科芯创展3 小时前
XZ4058B/C,20V,外置MOS,8.4V/8.7V开关充电芯片 宽范围电源电压:8.9V~20V-(电池充电电压:8.4V/8.7V)
c语言·开发语言
AI玫瑰助手3 小时前
Python流程控制:break与continue语句的区别与应用
开发语言·python·信息可视化
largecode4 小时前
如何让电话显示店名?来电显示店铺名称,提升有效接通率
java·开发语言·spring·百度·学习方法·业界资讯·twitter
xuhaoyu_cpp_java4 小时前
SpringMVC学习(五)
java·开发语言·经验分享·笔记·学习·spring