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

相关推荐
进击的荆棘2 小时前
优选算法——模拟
java·开发语言·算法·模拟
蓝天智能2 小时前
CMakeLists.txt配置详细介绍
c语言·开发语言·qt
0 0 02 小时前
CCF-CSP 36-2 梦境巡查(dream)【C++】考点:前缀和
开发语言·c++·算法
徐子童2 小时前
ArrayList和LinkedList的区别
java·开发语言·数据结构·高频面试题
fengxin_rou2 小时前
redis主从和集群一致性、哨兵机制详解
java·开发语言·数据库·redis·缓存
Olafur_zbj2 小时前
【AI】LLM上下文拼接
java·开发语言·spring·llm·context
leo__5202 小时前
基于C#与HALCON开发的完整视觉检测系统案例
开发语言·c#·视觉检测
猿饵块2 小时前
python--sys
开发语言·python
故河2 小时前
Python工具:Conda 包管理器
开发语言·python·conda