06 函数·方法·defer

Go 函数与方法速查:多返回值、命名返回、变参、闭包、方法集与接收者选择、defer 求值时机与执行顺序、panic/recover

06 函数·方法·defer

本页回答:返回值怎么设计、接收者该用值还是指针、defer 到底什么时候求值、panic 什么时候能用。

基线:Go 1.27.1。所有输出为本机实跑结果。


函数声明的全部形态

flowchart TB
    F(["函数声明"])
    F --> F1["普通函数<br/>func Name(params) (results)"]
    F --> F2["方法<br/>func (r T) Name(params) results"]
    F --> F3["函数字面量(闭包)<br/>func(params) results { }"]
    F --> F4["函数类型<br/>type Handler func(w, r)"]
    F --> F5["泛型函数<br/>func Map[T, U any](xs []T) []U"]
    F --> F6["无函数体(汇编/外部实现)<br/>func add(a, b int) int 🚧"]

    F1 --> P1["变参:func Sum(ns ...int)"]
    F2 --> P2["接收者:值 T 或指针 *T 🔥"]
    F3 --> P3["闭包捕获外部变量"]
    F5 --> P4["见 10 泛型"]
形态签名示例备注
普通函数func Add(a, b int) int同类型参数可合并写
多返回值func Div(a, b int) (int, error)Go 的「错误处理」基础 🔥
命名返回func F() (n int, err error)可被 defer 修改
变参func Sum(ns ...int) int本质是切片,调用时可展开
方法func (u *User) Save() error接收者写在 func 与名字之间
函数值var f func(int) int = double函数是一等公民
闭包add := func(x int) int { return x + n }捕获外部变量
泛型函数func Map[T, U any](xs []T, f func(T) U) []U见 10

参数与返回值

多返回值 + 命名返回

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 最常见:值 + 错误
func Div(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

// 命名返回:适合需要 defer 修改返回值的场景
func Parse(s string) (n int, err error) {
	defer func() {
		if err != nil {
			n = -1          // ✅ defer 能改命名返回值
		}
	}()
	if s == "" {
		return 0, errors.New("empty")
	}
	return len(s), nil
}
1
2
r, e := namedReturn()       // 内部 return 42, fmt.Errorf("boom")
fmt.Println("namedReturn:", r, e)
1
namedReturn: -1 boom
返回写法特点
return a, b最直白,推荐默认使用
return(裸返回)只在命名返回且函数极短时用 ⚠️ 长函数里是灾难
命名返回值价值在于被 defer 修改,而不是省几个字符 🔥

⚠️ 命名返回值会参与「裸返回」,长函数里裸返回会让读者必须回头找变量名。💭 惯例是:只有需要 defer 修改返回值时才命名,否则一律显式 return。

变参

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func Sum(ns ...int) int {
	total := 0
	for _, n := range ns {
		total += n
	}
	return total
}

Sum()              // 0,可以一个都不传
Sum(1, 2, 3)       // 6
xs := []int{1, 2}
Sum(xs...)         // ✅ 展开切片
细节说明
ns 的类型[]int,函数内就是个普通切片
不传参ns == nil,len(ns) == 0 ✅
传切片必须加 ...,且元素类型必须精确匹配 🛑
已有 []any 展开给 ...any✅ 可以;给 ...int 不行
只能有一个变参,且必须在最后编译器强制

⚠️ 变参不会自动转换类型:Sum([]any{1, 2}...) 编译错误,因为 []any 不是 []int。

函数作为值与类型

1
2
3
4
5
6
7
8
9
type Middleware func(http.Handler) http.Handler

func double(x int) int { return x * 2 }

var op func(int) int = double
op(21)   // 42

// 立即执行
func() { fmt.Println("IIFE") }()

⚠️ 函数类型只能与 nil 比较,不能互相比较 🛑:

1
2
3
var f1, f2 func()
fmt.Println(f1 == nil)   // ✅ true
// fmt.Println(f1 == f2) // 🛑 编译错误

方法:接收者怎么选

这是 Go 里最需要「一次想清楚」的设计决策。先看方法集规则,它决定接口满足关系:

flowchart TB
    subgraph MS["方法集(Method Set)"]
        T["类型 T 的方法集<br/>= 所有值接收者方法"]
        PT["类型 *T 的方法集<br/>= 值接收者方法 + 指针接收者方法"]
    end
    T --> CALL1["T 的变量可以调用<br/>值方法 ✅<br/>指针方法 ✅(若可寻址,自动 &v)"]
    PT --> CALL2["*T 的变量可以调用<br/>全部方法 ✅"]

    T --> IFACE1["接口要求值方法 →<br/>T 和 *T 都满足 ✅"]
    PT --> IFACE2["接口要求指针方法 →<br/>只有 *T 满足 ⚠️"]
表达式规则
v.ValueMethod()✅
v.PointerMethod()✅ 仅当 v 可寻址(变量、切片元素、可寻址结构体字段)
p.ValueMethod()✅ 自动解引用 (*p).ValueMethod()
p.PointerMethod()✅
T{}.PointerMethod()🛑 编译错误:不可寻址
f().PointerMethod()🛑 编译错误:函数返回值不可寻址
m["k"].PointerMethod()🛑 编译错误:map 元素不可寻址 ⚠️
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Value struct{ N int }

func (v Value) SetVal(n int) { v.N = n }   // 改的是副本
func (v *Value) SetPtr(n int) { v.N = n }  // 改的是原件

v := Value{N: 1}
v.SetVal(99)
fmt.Println("after SetVal:", v.N)   // 1,没变 ⚠️
v.SetPtr(99)
fmt.Println("after SetPtr:", v.N)   // 99

// map 元素不可寻址的绕法
m := map[string]Value{"a": {N: 1}}
// m["a"].SetPtr(5)   // 🛑 编译错误
tmp := m["a"]
tmp.SetPtr(5)
m["a"] = tmp           // ✅ 取出 → 改 → 放回
fmt.Println("map workaround:", m["a"].N)   // 5

// 切片元素可寻址,可以直接调
xs := []Value{{N: 1}}
xs[0].SetPtr(7)
fmt.Println("slice elem:", xs[0].N)        // 7
1
2
3
4
after SetVal: 1
after SetPtr: 99
map workaround: 5
slice elem: 7

决策表:用值接收者还是指针接收者

情况选择理由
方法要修改接收者指针 *T唯一选择 🔥
结构体较大(> 几个字段)指针 *T避免每次调用拷贝
结构体含 sync.Mutex/sync.WaitGroup必须指针复制锁是 bug ⚠️
类型本质是「值」(time.Time、小坐标)值 T不可变更自然
类型是 map/slice/chan/func通常值 T本身就是引用语义
需要满足某个接口看接口要求的方法集见上图
同类型已有指针方法统一用指针混用会造成方法集分裂 ⚠️

⚠️ 黄金规则:同一个类型的所有方法,接收者形式保持一致。混用会让 T 和 *T 的方法集不同,埋下「为什么这个类型不满足接口」的坑。

值接收者也会拷贝

1
2
3
4
type Big struct{ data [1 << 20]byte }   // 1 MB

func (b Big) Sum() int   { return len(b.data) }   // ⚠️ 每次调用拷贝 1 MB
func (b *Big) Sum2() int { return len(b.data) }   // ✅ 不拷贝

go vet 不会查这个,但代码审查要看一眼。💭 经验阈值:结构体超过 34 个机器字(约 2432 字节)就考虑指针接收者。


defer:三个必须记住的语义

语义一:LIFO 顺序,函数返回前执行

1
2
3
4
for i := range 3 {
	defer fmt.Print("defer ", i, " | ")
}
fmt.Print("body done | ")
1
body done | defer 2 | defer 1 | defer 0

所以「先 defer 的先执行」是错的:后 defer 的先执行(栈)。

语义二:参数立即求值,闭包延迟求值

这是 defer 最容易错的地方,务必分清:

1
2
3
4
5
x := 1
defer fmt.Println("arg evaluated immediately:", x)   // x 在此刻被拷贝
defer func() { fmt.Println("closure sees final:", x) }()  // x 在返回时才读
x = 100
fmt.Println("x set to", x)
1
2
3
x set to 100
closure sees final: 100
arg evaluated immediately: 1
sequenceDiagram
    participant F as 函数体
    participant D as defer 栈
    F->>D: defer fmt.Println(x) → 参数 x=1 立刻求值拷贝
    F->>D: defer func(){...}() → 只记录闭包
    F->>F: x = 100
    F->>F: return
    F->>D: 弹栈执行(LIFO)
    D-->>F: 闭包读 x → 100
    D-->>F: 打印第 1 个 defer 的参数 → 1
写法求值时机
defer f(x)x 在 defer 语句执行时求值 🔥
defer func() { use(x) }()x 在函数返回时读取
defer f() 中 f 本身也在 defer 时确定(函数值不能为 nil)⚠️

🛑 经典错误:defer f.Close() 在循环里累积,或者 defer 的实参是个会变的指针:

1
2
3
for _, name := range names {
	defer os.Remove(name)   // ⚠️ 参数立即求值,但函数结束才删——可能删错时刻
}

语义三:defer 能改命名返回值

1
2
3
4
5
6
7
8
func namedReturn() (result int, err error) {
	defer func() {
		if err != nil {
			result = -1
		}
	}()
	return 42, fmt.Errorf("boom")
}
1
namedReturn: -1 boom

这是唯一让 defer 影响返回值的途径。常见用法:

用途代码
统一错误包装defer func() { if err != nil { err = fmt.Errorf("ctx: %w", err) } }()
统一耗时统计defer func(start time.Time) { log.Printf("took %v", time.Since(start)) }(time.Now())
panic 转 errordefer func() { if r := recover(); r != nil { err = ... } }()
释放资源defer f.Close() 🔥

defer 的开销

Go 1.14 起 defer 采用开放编码(open-coded defers),在函数内 defer 数量少且没有循环时几乎是零开销(不分配、可内联)。

flowchart LR
    subgraph OLD["Go 1.13 及以前"]
        O1["每次 defer<br/>堆分配 _defer 结构 ⚠️"]
    end
    subgraph NEW["Go 1.14+"]
        N1["静态 defer(无循环)<br/>编译期内联到栈帧 ✅"]
        N2["循环内/动态 defer<br/>仍走堆分配 ⚠️"]
    end
    NEW --> ADVICE["💭 结论:正常使用 defer,<br/>只有超热路径的循环内 defer 才需要担心"]

panic 与 recover

运行顺序

stateDiagram-v2
    [*] --> Normal: 函数开始
    Normal --> Defer: return 或 panic
    Defer --> Defer: 依次执行 defer(LIFO)
    Defer --> Recovered: 某个 defer 里 recover() 返回非 nil
    Defer --> Panicking: 没有 recover,或 recover 后又 panic
    Recovered --> NormalReturn: 函数【正常返回】给调用方 ✅
    Panicking --> CallerDefer: 向上传播,执行调用方的 defer
    CallerDefer --> [*]: 无人 recover → 程序崩溃,打印栈
    NormalReturn --> [*]
1
2
3
4
5
6
7
8
9
func recoverDemo() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recovered:", r)
		}
		fmt.Println("after recover, function returns normally")
	}()
	panic("something bad")
}
1
2
recovered: something bad
after recover, function returns normally

关键点:recover() 之后该 defer 函数继续执行完,然后外层函数正常返回(不是继续执行 panic 之后的代码)。

recover 只在 defer 里有效

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 🛑 无效:recover 直接调用
func bad() {
	if r := recover(); r != nil { }   // 永远返回 nil
}

// 🛑 无效:在 defer 调用的【另一个函数】里 recover
defer func() { helper() }()           // helper 里的 recover 返回 nil

// ✅ 有效:recover 必须在 defer 的函数字面量里【直接】调用
defer func() { if r := recover(); r != nil { } }()

panic 的传播与再抛

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func nestedPanic() {
	defer fmt.Println("outer defer runs")
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("inner recovered:", r)
			panic("re-panic from defer")
		}
	}()
	panic("original")
}
1
2
3
inner recovered: original
outer defer runs
caught re-panic: re-panic from defer

注意执行顺序:内层 defer 先跑(LIFO)并 recover;它再 panic 后,同级剩下的 defer(“outer defer runs”)仍然会执行,然后才向上传播。

panic 转 error:库函数的标准做法

1
2
3
4
5
6
7
8
func sentinelErr() (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("converted panic: %v", r)
		}
	}()
	panic("kaboom")
}
1
sentinelErr: converted panic: kaboom

💡 库代码里应该这样做:对外的 API 不应该把 panic 泄漏给调用方,而是用命名的 err 接住并转成错误。⚠️ 但要注意:如果 panic 是 runtime.Error(如空指针、越界),通常应该继续传播,因为那说明程序有 bug,掩盖它更危险。

panic/recover 使用边界

场景是否该用
库函数边界统一转 error✅ 推荐
服务器每个请求的兜底(避免单请求崩溃整个进程)✅ 推荐
init() 里配置加载失败✅ 可用(配置错误应让程序起不来)
参数校验失败🛑 用 error 返回
业务逻辑分支控制🛑 极其反模式
文件不存在、网络超时🛑 用 error
数组越界、nil 解引用🛑 不要 recover,这是 bug
跨 goroutine 传播 panic🛑 做不到:每个 goroutine 必须自己 recover ⚠️

⚠️ panic 不会跨 goroutine 传播:一个 goroutine panic 且未 recover,整个进程崩溃。所以每个长期运行的 goroutine 顶部都该有 recover(除非你确定它不会 panic)。


闭包

闭包 = 函数字面量 + 它捕获的变量环境。Go 的闭包按引用捕获。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func counter() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}

c := counter()
fmt.Println(c(), c(), c())   // 1 2 3
c2 := counter()
fmt.Println(c2())            // 1,独立的环境
特点说明
捕获方式按引用(变量本身),不是拷贝副本
生命周期被捕获的变量逃逸到堆,随闭包存活
循环变量🆕 1.22 起每次迭代新建,闭包各捕获各的(见 05)
常见用途中间件、选项模式、延迟求值、记忆化

闭包捕获的是变量不是值

1
2
3
4
5
x := 1
inc := func() { x++ }
read := func() int { return x }
inc()
fmt.Println(read())   // 2,两个闭包共享同一个 x

⚠️ 共享可变状态是闭包最容易出的问题。并发下要加锁,或改用参数传递:

1
2
3
4
5
6
// 🛑 并发下数据竞争
var wg sync.WaitGroup
for i := range 10 {
	wg.Add(1)
	go func() { defer wg.Done(); counter++ }()   // counter 竞争
}

本页陷阱速查

症状实际原因正确做法
值接收者方法改了字段没生效改的是副本用指针接收者 *T
m["k"].SetX() 编译错误map 元素不可寻址取出 → 改 → 放回
f().PointerMethod() 编译错误函数返回值不可寻址先赋给变量
某类型不满足接口接口要指针方法,传的是值传 &v,或统一用值接收者
defer 里打印的变量是旧值defer 的参数立即求值用闭包 func(){ ... }()
defer 里拿不到最新值用了参数形式而非闭包同上
defer 修改返回值无效返回值没有命名改成 (result T, err error)
recover() 返回 nilrecover 不在 defer 直接调用写在 defer func(){ ... }() 体内
一个 goroutine panic 整个服务挂掉panic 不跨 goroutine 传播每个 goroutine 自己 recover
循环里 defer Close() 累积几百个defer 在函数返回时才执行把循环体抽成函数
变参函数收到 []any 编译失败变参不做类型转换逐个传,或改签名
函数值 == 比较编译错误函数只能与 nil 比较用 reflect.ValueOf(f).Pointer() 或改设计

📘 官方参考:Spec — Function declarations、Spec — Method sets、Go Blog — Defer, Panic, and Recover、Go 1.14 open-coded defers

➡️ 上一节:05 语句与控制流 | 下一节:07 数组·切片·映射·结构体

最后修改 September 21, 2026: 更新 (ac821931b)