最近我在学 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 执行顺序
整体顺序可以记成:
-
先初始化依赖包(被依赖的先初始化)
-
再初始化当前包的全局变量(按声明顺序)
-
最后执行 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 的区别当成必过关卡:这俩是最容易"看起来会了但实战翻车"的点。