当前位置: 首页 > news >正文

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

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


目录

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

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

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

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

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

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

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

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.1new:给你一块“零值内存”,并返回指针

p := new(int) // p 是 *int,且 *p == 0 *p = 5

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

下面这个就是经典踩坑:

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

http://www.jsqmd.com/news/468496/

相关文章:

  • RabbitMQ整合springboot
  • Java基于微信小程序的社区垃圾回收管理系统【附源码、文档说明】
  • 2026年知网AIGC检测不通过?这4款降AI率工具亲测有效
  • 2026年东北乡土苗木标杆基地最新推荐:云杉营养钵苗、东北红松苗、红松小苗、红松大苗1-6米高、红松营养钵苗、水曲柳苗、靖宇县宜达苗木基地,筑牢绿化种植品质根基 - 海棠依旧大
  • MCP Server简介
  • 大数据领域ClickHouse的缓存机制分析
  • 【OpenClaw】使用教程
  • C++中的访问者模式变体
  • cgroups实战:如何有效管理系统资源
  • 2026年3月靖宇县苗木基地最新推荐榜单:云杉、红松、水曲柳、云杉树苗、东北云杉、东北云杉大苗1-8米、营养钵云杉等苗木选择指南 - 海棠依旧大
  • 把音乐库搬上云端:Navidrome 自托管音乐服务器搭建指南
  • Flutter 三方库 pip 的鸿蒙化适配指南 - 实现标准化的画中画(Picture-in-Picture)模式、支持视频悬浮窗与多任务并行交互
  • 202603周赛新D题
  • Json在线工具使用说明
  • 上课听得懂 一考试就低分!这样选学习机 彻底打通 “会→对→高分” - 海淀教育研究小组
  • 基于ArcScene的裸眼立体图制作说明
  • 基于C++的爬虫框架
  • 查看QPS,根据QPS 对php-fpm.d中www.conf的设置
  • 企业管理系统前端组件化设计实战:OA、CRM、ERP 表单为什么不能直接用 Element UI / Ant Design?
  • ArcGIS中利用DEM制作立体晕渲图的说明
  • 嵌入式C++测试框架
  • 2026 审讯桌、审讯椅、调度台哪家强?UDWEN 优盾专业厂家实力领衔 - 品牌智鉴榜
  • pikachu靶场——SQL-Inject—1(Kali系统)
  • 腾讯地图加载详细说明 —— 以leaflet为例
  • C++游戏开发之旅29
  • 2026年方管标杆供应厂家最新推荐:Q235方管、Q355方管、无缝方管、钢结构方管、河南红宇供应链,品质方管适配多行业需求 - 海棠依旧大
  • ArcGIS Server发布的地图服务不显示地图的原因分析
  • ArcGIS中点转线面的方法
  • 2026年3月河南方管供应企业最新推荐榜单:镀锌方管、黑方管、镀锌方矩管、热镀锌方管、热镀方矩管、各类方管、方矩管采购选择指南 - 海棠依旧大
  • 基本元器件——比较器