函数
# 第 7 章 函数
Go 函数是一等公民:多返回值、命名返回、变长参数、闭包、
defer、函数类型。 关键词:多返回值、error返回、命名返回值、defer求值时机、闭包变量捕获、函数式选项
# 目录介绍
- 7.1 本章学习目标
- 7.2 函数定义与签名
- 7.3 多返回值与
error模式 - 7.4 命名返回值与裸 return
- 7.5 变长参数
...T - 7.6 函数作为一等公民
- 7.7 闭包
- 7.8 defer:函数返回前调用
- 7.9 init 函数
- 7.10 综合示例:函数式选项模式
- 7.11 本章底层原理(简介)
- 7.12 Go 新手陷阱 Top 5
- 7.13 思考题
- 7.14 推荐阅读
# 7.1 本章学习目标
- ✅ 写出 Go 多返回值、命名返回值、变长参数三种签名
- ✅ 能解释
defer的两阶段:参数何时求值、函数何时执行 - ✅ 能用
defer + recover在边界处兜住 panic - ✅ 能写函数式选项模式(Functional Options)
- ✅ 知道闭包变量捕获的"陷阱根源"——它捕获的是变量本体,不是当时的值
- ✅ 知道
init何时被调用、包初始化顺序、为什么禁止显式调用
# 7.2 函数定义与签名
# 7.2.1 基本语法与零参零返回
func 函数名(参数列表) (返回值列表) {
函数体
}
2
3
最小例子:
func hello() {
fmt.Println("hi")
}
func add(a int, b int) int {
return a + b
}
2
3
4
5
6
7
返回值只有一个时,括号可省。零返回值时整个返回值列表都不写。
# 7.2.2 参数列表的简写规则
相邻同类型参数可合并写一次类型:
// 啰嗦写法
func add(a int, b int, c int) int { return a + b + c }
// 合并写法
func add(a, b, c int) int { return a + b + c }
// 多类型混合
func split(s string, sep byte) (left, right string) {
// ...
}
2
3
4
5
6
7
8
9
10
这种简写在标准库中无处不在,强烈建议团队统一用合并写法——更紧凑、更符合 Go 风格。
# 7.2.3 没有默认参数与重载
Go 故意砍掉了两个 C++/Python/Java 都有的特性:
| 特性 | C++/Python | Go |
|---|---|---|
| 默认参数 | func f(x int=10) | ❌ 不支持 |
| 函数重载 | 同名多签名 | ❌ 不支持 |
为什么? Rob Pike 在 Go FAQ (opens new window) 写:
Method dispatch is simplified if it doesn't need to do type matching as well. Experience with other languages told us that having a variety of methods with the same name but different signatures was occasionally useful but it could also be confusing and fragile in practice.
替代方案:
- 默认参数 → 用"函数式选项模式"(见 §7.10)或多个不同名函数
- 重载 → 不同名(
AddInt、AddFloat)或泛型(Go 1.18+)
// 替代重载:泛型
func Add[T int | float64 | string](a, b T) T { return a + b }
2
# 7.3 多返回值与 error 模式
# 7.3.1 多返回值是 Go 的标志
func divmod(a, b int) (int, int) {
return a / b, a % b
}
q, r := divmod(17, 5)
fmt.Println(q, r) // 3 2
2
3
4
5
6
C 用"指针出参"模拟多返回值,Python 用 tuple 包装,Go 直接在语言层支持——这是 Go 与 C 系语言最显眼的语法差异之一。
底层上,多返回值是通过栈上预留多个返回槽实现的,零额外开销(不分配 tuple、不做 boxing)。
# 7.3.2 (value, error) 双返回的工程惯例
Go 几乎所有有失败可能的 API 都遵循这个签名:
func 函数名(...) (结果, error)
经典调用模式:
data, err := os.ReadFile("config.yaml")
if err != nil {
return fmt.Errorf("read config: %w", err)
}
// 用 data
2
3
4
5
强制规则(团队 Code Review 关注点):
| 规则 | 反例 | 正例 |
|---|---|---|
error 永远是最后一个返回值 | (error, []byte) | ([]byte, error) |
err != nil 时其他返回值不可信 | 看到 err 还用 data | 立即 return |
| 错误一定要处理(return / log / 转译) | data, _ := ... 静默吞 | errors.Is/As、%w 包装 |
详见 11. 错误处理。
# 7.3.3 用 _ 忽略不需要的返回值
_, err := fmt.Println("hello")
if err != nil { ... }
// 完全忽略 err(仅当确实不在乎,比如 print 到 stderr)
fmt.Println("debug:", x)
2
3
4
5
⚠️ _, _ := ... 是 Go 里最危险的写法之一——多数情况都意味着"我懒得处理错误"。Code Review 要求每个 _ 处都有注释说明"为什么忽略"。
# 7.4 命名返回值与裸 return
# 7.4.1 命名返回值的语法
func divmod(a, b int) (q, r int) {
q = a / b
r = a % b
return // 裸 return,自动返回当前 q, r
}
2
3
4
5
命名返回值在函数开头就被声明并初始化为零值,相当于"参数列表的延伸"。
它的两个作用:
- 作为文档:调用方一眼看出每个返回值的含义
func parseHostPort(s string) (host string, port int, err error) // 比 (string, int, error) 自描述1
2 - 配合 defer 修改返回值(见 §7.8.3)
# 7.4.2 何时该用命名返回值
// ✅ 多个同类型返回值,名字消歧义
func split(s string) (left, right string)
// ✅ 复杂函数 + defer 改 err
func process() (result *Result, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
// ...
return result, nil
}
// ❌ 简单函数没必要
func add(a, b int) (sum int) {
sum = a + b
return // 多此一举,直接 return a + b 更清晰
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
经验法则:
- 函数 < 10 行 + 只有一个返回值 → 不用命名返回
- 多返回值且类型不直观(如
(string, string, error))→ 用命名返回 - 需要 defer 改 err → 必用命名返回
⚠️ 裸 return 在长函数里是反模式——超过 30 行的函数还用裸 return,读者要往上翻找当前每个变量的值,可读性下降。只在短函数里用裸 return。
# 7.5 变长参数 ...T
# 7.5.1 定义与调用
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum() // 0
sum(1, 2, 3) // 6
sum(1, 2, 3, 4, 5) // 15
2
3
4
5
6
7
8
9
10
11
函数内部 nums 是 []int——变长参数本质是一个 slice 包装。...T 必须是参数列表的最后一个。
最常见的标准库变长参数:
fmt.Println(args ...any)
fmt.Sprintf(format string, args ...any)
strings.Join(elems []string, sep string) // ❌ 不是变长,是 slice
append(s []T, vs ...T) []T // ✅ 内置 append
2
3
4
# 7.5.2 把 slice 展开传入
nums := []int{1, 2, 3, 4, 5}
total := sum(nums...) // ← 注意尾随的 ...
2
nums... 的含义:把 slice 元素逐个作为参数传入,等价于 sum(1, 2, 3, 4, 5)。
⚠️ 常见误区——不能"半展开":
sum(0, nums...) // ❌ 编译错:太多参数
sum(append([]int{0}, nums...)...) // ✅ 先合并成 slice 再展开
2
也别忘了——... 后的 slice 与函数内部共享底层数组:
nums := []int{1, 2, 3}
modify := func(args ...int) { args[0] = 999 }
modify(nums...)
fmt.Println(nums) // [999 2 3] ← 被改了!
2
3
4
要安全请先 append([]int(nil), nums...) 或 slices.Clone(nums) 复制。
# 7.6 函数作为一等公民
"一等公民"指:函数可以被赋值给变量、作为参数传递、作为返回值返回、被存进 slice/map。Go 完全支持。
# 7.6.1 函数类型与函数变量
// 函数类型
type BinOp func(a, b int) int
// 函数变量
var op BinOp = func(a, b int) int { return a + b }
fmt.Println(op(3, 4)) // 7
// 函数赋值
op = func(a, b int) int { return a * b }
fmt.Println(op(3, 4)) // 12
// 零值是 nil
var op2 BinOp
op2(1, 2) // ❌ panic: invalid memory address or nil pointer dereference
2
3
4
5
6
7
8
9
10
11
12
13
14
两个函数类型相同的判定:参数类型列表 + 返回值列表完全一致。参数名不影响类型。
# 7.6.2 函数作为参数(回调)
func mapInts(s []int, f func(int) int) []int {
result := make([]int, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
doubled := mapInts([]int{1, 2, 3}, func(x int) int { return x * 2 })
fmt.Println(doubled) // [2 4 6]
2
3
4
5
6
7
8
9
10
标准库经典回调:
sort.Slice(items, func(i, j int) bool { return items[i].ID < items[j].ID })
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { ... })
filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { ... })
2
3
# 7.6.3 函数作为返回值(高阶函数)
// 返回一个"加 n 的函数"
func adder(n int) func(int) int {
return func(x int) int { return x + n }
}
add10 := adder(10)
fmt.Println(add10(5)) // 15
fmt.Println(add10(20)) // 30
2
3
4
5
6
7
8
adder 返回的内部函数捕获了外层变量 n——这就是闭包,下节详讲。
# 7.7 闭包
闭包 = 函数 + 它捕获的外部变量。Go 的闭包是按引用捕获——这点是所有"闭包陷阱"的根源。
# 7.7.1 闭包捕获引用而非值
func counter() func() int {
n := 0
return func() int {
n++ // 捕获的是 n 本体
return n
}
}
c := counter()
fmt.Println(c(), c(), c()) // 1 2 3
2
3
4
5
6
7
8
9
10
n 本来是 counter 的栈变量,但因为内部函数把它"带走了",编译器会把它逃逸到堆——这样多次调用都共享同一个 n。
两个独立 counter 互不干扰:
c1 := counter()
c2 := counter()
fmt.Println(c1(), c1(), c2()) // 1 2 1
2
3
每次调用 counter() 都生成新的闭包实例(新的 n)。
# 7.7.2 闭包导致的逃逸
func makePrinter(prefix string) func(string) {
// prefix 捕获到闭包,逃逸到堆
return func(s string) {
fmt.Println(prefix + ": " + s)
}
}
p := makePrinter("[INFO]")
p("started") // [INFO]: started
2
3
4
5
6
7
8
9
可以用 go build -gcflags="-m" 看到逃逸报告:
./main.go:5:15: leaking param: prefix
./main.go:6:9: func literal escapes to heap
2
这意味着:闭包不是"零成本"的——每个闭包是一个堆对象,含一个函数指针 + 捕获的变量集合。性能敏感的内层循环里,能不用闭包就别用。
➡ 卷三第 8 章 闭包与逃逸 详讲。
经典闭包陷阱(Go 1.22 起已修复,详见 6.3.6):
// Go 1.21 及之前
fns := []func(){}
for _, v := range []int{1, 2, 3} {
fns = append(fns, func() { fmt.Println(v) })
}
// 全部输出 3——三个闭包共享同一个 v
2
3
4
5
6
# 7.8 defer:函数返回前调用
defer 是 Go 最有标志性的工程语法——把一个函数调用"挂起",等当前函数返回前再执行。最常见的用途是"成对操作的清理":
func readConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // 无论怎么 return,f 一定被关闭
return io.ReadAll(f)
}
2
3
4
5
6
7
8
9
它替代了 C 的 goto cleanup 与 Java/Python 的 try-finally——比 try-finally 更紧贴资源获取点,可读性更好。
# 7.8.1 defer 求值时机:参数立即求值
defer 的语义有两个时间点:
- defer 语句执行时:参数立即求值并保存
- 函数返回前:用保存的参数调用函数
func main() {
x := 1
defer fmt.Println("deferred x =", x) // x 立即求值 = 1
x = 999
fmt.Println("normal x =", x)
}
// 输出:
// normal x = 999
// deferred x = 1 ← 不是 999!
2
3
4
5
6
7
8
9
经典误用——计时:
// ❌ 想计耗时?这样不对
defer fmt.Println("elapsed:", time.Since(start))
// time.Since(start) 在 defer 行立即求值,结果是 0
// ✅ 包一层闭包,把时间戳计算推迟到调用时
defer func() {
fmt.Println("elapsed:", time.Since(start))
}()
2
3
4
5
6
7
8
记住口诀:裸调用立即求值,闭包推迟求值。
# 7.8.2 多个 defer 后进先出
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// 输出:
// 3
// 2
// 1
2
3
4
5
6
7
8
9
这是"栈"的语义——和函数调用的清理顺序天然吻合:先获取的资源最后释放。
# 7.8.3 defer 修改返回值(命名返回)
这是命名返回值 + defer最强大的组合——defer 函数能读写命名返回值:
func divide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r) // ✅ 改写命名返回 err
}
}()
return a / b, nil // b=0 时 panic,被 defer 兜住
}
result, err := divide(10, 0)
fmt.Println(result, err) // 0 recovered: runtime error: integer divide by zero
2
3
4
5
6
7
8
9
10
11
为什么能改? 命名返回值在函数开头就分配了存储位置,return result, nil 等价于"先把 a/b 写入 result、把 nil 写入 err,然后跳到统一返回处"。defer 在跳到返回处之前执行,能改写已经赋值的命名返回。
匿名返回值就改不了:
func bad() int {
x := 10
defer func() {
x = 999 // ❌ 改的是局部 x,不是返回值
}()
return x // 返回的是 x 当时的值 10,被复制走了
}
fmt.Println(bad()) // 10
2
3
4
5
6
7
8
# 7.8.4 defer 与 panic / recover
panic 触发"当前 goroutine 立即终止"流程:
- 当前函数立即停止
- 已注册的
defer按 LIFO 顺序执行 - 把 panic 向上抛给调用者
- 一直抛到
main函数,整个程序崩溃打印堆栈
recover() 是唯一能在 panic 流程中拦截的机制,且必须在 defer 函数里直接调用:
func safeCall() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered: %v\n%s", r, debug.Stack())
}
}()
doSomethingDangerous() // 即使 panic 了也不会让程序崩溃
}
2
3
4
5
6
7
8
⚠️ 只在边界处用 recover——典型场景:
| 场景 | 为什么用 |
|---|---|
| HTTP / RPC handler 入口 | 一个请求出 bug 不能让整个服务挂 |
| goroutine worker 入口 | 一个 worker 崩了不能拖垮全程序 |
| 第三方库的 cgo / unsafe 边界 | 隔离不可控的崩溃 |
| 测试框架的 setup/teardown | 收集失败信息 |
业务代码内禁止 recover——它会把"程序员该修的 bug"伪装成"运行时错误"。详见 11. 错误处理。
# 7.8.5 defer 的性能成本(Go 1.14+ 已大幅优化)
老 Go(< 1.13)的 defer 用"链表挂在 g 结构上"实现,每次约 50ns 开销,热点循环里能感知到。
Go 1.14 引入"open-coded defer"——编译器在能静态确定 defer 数量(≤ 8 个)的函数里,把 defer 直接展开成普通调用,开销趋近于零。详见 Go 1.14 release notes (opens new window)。
实务结论:
- 普通业务代码:放心用
defer,性能开销可忽略 - 极致热点循环(如每秒百万次的解析器):仍要 benchmark 验证,必要时手动展开
➡ 卷三第 14 章 defer 实现机制 详讲。
# 7.9 init 函数
Go 在每个 .go 文件里允许定义一个或多个特殊函数 init,它们在 main 之前自动执行:
package config
var Conf *Config
func init() {
Conf = loadFromEnv()
}
2
3
4
5
6
7
6 条核心规则:
- 签名必须是
func init()——零参、零返回 - 不能被显式调用(连
init()自己都不行——编译错) - 一个文件可以有多个
init,按源码顺序执行 - 一个包可以跨多个文件有多个
init,按文件名字典序执行 - 包初始化顺序:依赖优先——A 依赖 B,B 的
init先于 A init在所有包变量初始化之后、main之前
整体启动顺序:
被导入的包(按依赖图拓扑序)
↓ 每个包内:
1. 包级变量初始化(按依赖序)
2. init() 函数(按源文件 + 文件内顺序)
↓
main 包的 init
↓
main()
2
3
4
5
6
7
8
典型用途:
// 注册 SQL 驱动
import _ "github.com/lib/pq" // 这个包的 init() 调 sql.Register("postgres", ...)
// 命令行子命令注册
func init() {
cmd.Register("migrate", migrateCmd)
}
// 编译期类型断言(确保 *MyType 实现了 MyInterface)
var _ MyInterface = (*MyType)(nil)
2
3
4
5
6
7
8
9
10
反模式:
| 反模式 | 为什么不好 |
|---|---|
init 里读配置文件、连数据库 | 启动失败时栈追踪不清晰;难单测 |
init 里 panic | 程序起不来,且无法 recover |
init 隐式依赖另一个包的 init 已经跑过 | 循环依赖时初始化顺序不可预期 |
多个 init 互相依赖文件名字典序 | 重命名文件会破坏初始化 |
最佳实践:init 只做"注册自己"这种纯本地动作,复杂初始化放 func New() 显式构造,由调用方控时机。
# 7.10 综合示例:函数式选项模式
这是 Go 最经典的设计模式,由 Rob Pike 与 Dave Cheney 共同推广,现在标准库(http.Server 配置)和大部分第三方库都用它。
问题场景:构造一个对象,参数有十多个,且大多数有合理默认值。
C++ 用默认参数,Java 用 Builder,Go 没默认参数也不流行 Builder——它用函数式选项:
// server/server.go
package server
import "time"
type Server struct {
addr string
port int
readTimeout time.Duration
writeTimeout time.Duration
maxConns int
tlsCert string
}
// Option 是修改 Server 的函数类型
type Option func(*Server)
// 一组选项构造器
func WithAddr(addr string) Option {
return func(s *Server) { s.addr = addr }
}
func WithPort(port int) Option {
return func(s *Server) { s.port = port }
}
func WithTimeouts(read, write time.Duration) Option {
return func(s *Server) {
s.readTimeout = read
s.writeTimeout = write
}
}
func WithMaxConns(n int) Option {
return func(s *Server) { s.maxConns = n }
}
func WithTLS(certPath string) Option {
return func(s *Server) { s.tlsCert = certPath }
}
// New 构造函数,应用所有选项
func New(opts ...Option) *Server {
// 1. 默认值
s := &Server{
addr: "0.0.0.0",
port: 8080,
readTimeout: 30 * time.Second,
writeTimeout: 30 * time.Second,
maxConns: 1000,
}
// 2. 应用用户传入的选项(覆盖默认值)
for _, opt := range opts {
opt(s)
}
return s
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
调用方就地组合选项:
// 全部用默认
srv1 := server.New()
// 只改端口
srv2 := server.New(server.WithPort(9090))
// 改端口 + 加 TLS + 调超时
srv3 := server.New(
server.WithPort(443),
server.WithTLS("/etc/cert.pem"),
server.WithTimeouts(60*time.Second, 60*time.Second),
)
2
3
4
5
6
7
8
9
10
11
12
优势:
| 维度 | 函数式选项 | 直接传 struct | Builder 链式 |
|---|---|---|---|
| 默认值 | ✅ 函数内集中定义 | ⚠️ 调用方自己填零值 | ✅ |
| 向后兼容(加字段) | ✅ 加 WithXxx 即可 | ❌ 调用方都要改 | ✅ |
| 强制必填 | ✅ 提到位置参数 | ⚠️ 难以校验 | ⚠️ |
| 可读性 | ✅ 选项名自描述 | ⚠️ 大括号匿名字段 | ✅ |
| IDE 自动补全 | ✅ | ✅ | ✅ |
| 性能(每选项 1 个闭包) | ⚠️ 启动期一次性,无影响 | ✅ 零开销 | ⚠️ |
进阶版本——选项可返回 error:
type Option func(*Server) error
func WithTLS(certPath string) Option {
return func(s *Server) error {
if _, err := os.Stat(certPath); err != nil {
return fmt.Errorf("WithTLS: %w", err)
}
s.tlsCert = certPath
return nil
}
}
func New(opts ...Option) (*Server, error) {
s := &Server{ /* defaults */ }
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, err
}
}
return s, nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
grpc.ServerOption、http.Server 的初始化都遵循类似模式。
# 7.11 本章底层原理(简介)
| 主题 | 简介 | 详解卷三 |
|---|---|---|
| 函数调用约定 | 参数走栈(部分版本走寄存器,Go 1.17+ 引入 register-based ABI);返回值预留栈槽 | 第 5 章 |
defer 实现 | 老版本用链表(_defer struct 挂在 g 上);Go 1.14+ open-coded defer | 第 14 章 |
| 闭包结构 | 编译器生成 funcval struct(函数指针 + 捕获变量数组),堆分配 | 第 8 章 |
panic / recover | 沿 g 的 _defer 链向上展开(unwind),recover 标记 panic 已处理 | 第 14 章 |
| 函数值的 nil 比较 | funcval 指针为 0 即 nil;不能比较两个非 nil 函数值是否相等 | 第 5 章 |
# 7.12 Go 新手陷阱 Top 5
# ❌ 陷阱 1:defer file.Close() 在 for 循环里累积
for _, name := range names {
f, err := os.Open(name)
if err != nil { return err }
defer f.Close() // ❌ 1000 个文件全部攒到函数末尾才关
// 处理 f
}
2
3
4
5
6
修复:
// ✅ 方案 A:包一层函数立即关闭
for _, name := range names {
err := func() error {
f, err := os.Open(name)
if err != nil { return err }
defer f.Close()
// 处理 f
return nil
}()
if err != nil { return err }
}
// ✅ 方案 B:显式调用,去掉 defer
for _, name := range names {
f, err := os.Open(name)
if err != nil { return err }
// 处理 f
f.Close()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ❌ 陷阱 2:defer fmt.Println(time.Now()) 不是函数返回时求值
start := time.Now()
defer fmt.Println("elapsed:", time.Since(start)) // ❌ 立即求值 = 0
2
修复:
defer func() { fmt.Println("elapsed:", time.Since(start)) }() // ✅
# ❌ 陷阱 3:闭包捕获循环变量(Go 1.21 及之前)
for _, v := range items {
go func() { use(v) }() // ❌ 全部用最后一个 v
}
2
3
修复:升级 go.mod 到 go 1.22+,或显式:
for _, v := range items {
v := v
go func() { use(v) }()
}
2
3
4
# ❌ 陷阱 4:返回 non-nil interface 包了 nil 指针
type MyErr struct{ msg string }
func (e *MyErr) Error() string { return e.msg }
func bad() error {
var e *MyErr // nil
return e // ❌ 返回的 error 不为 nil!
}
if err := bad(); err != nil {
fmt.Println("got err") // ✅ 真的进来了
}
2
3
4
5
6
7
8
9
10
11
根因:error 是 interface,由(类型,值)二元组构成。(*MyErr, nil) 这个 interface 不是 nil interface((nil, nil) 才是)。详见 10. 接口与多态。
修复:
func ok() error {
var e *MyErr
if e == nil {
return nil // ✅ 显式返回 nil interface
}
return e
}
2
3
4
5
6
7
# ❌ 陷阱 5:init() 里 panic 启动失败
func init() {
db = mustConnect() // 启动期连数据库失败 → panic → 程序起不来
}
2
3
外部依赖(DB / Redis / 配置)不要放 init,改放 func New() 由 main 显式调用,便于:
- 单元测试 mock
- 启动失败时输出可控的错误信息
- 重试 / 降级
# 7.13 思考题
- Go 为什么不支持函数重载?请用一段代码说明"假如有重载,方法集合查找会变得多复杂"。
- 写一个
Once函数:func Once(f func()) func()包装传入函数,让其无论被调多少次只执行一次。提示:闭包 +sync.Once。 - 命名返回值 + defer 改 err 的组合最适合什么场景?请列举 3 个真实业务场景。
- 解释
defer的两阶段(参数求值时机 vs 调用时机)。如果 Go 改成"参数也推迟到函数返回时求值",会破坏哪些现有代码? - 函数式选项模式 vs 直接传
Config struct,哪个更适合"必填项多、可选项少"的场景?为什么? - 多个包的
init顺序由什么决定?两个互相不依赖的包的init顺序是稳定的吗?请查文档佐证。 - 写一个最小可运行例子,让 panic 在 goroutine 内被 recover 兜住但不影响主 goroutine 的正常退出。
# 7.14 推荐阅读
# 卷内
- 第 6 章 流程语句(控制流基础)
- 第 8 章 指针与逃逸(闭包变量逃逸)
- 第 10 章 接口与多态(nil interface 陷阱)
- 第 11 章 错误处理(error 模式 + recover 边界)
# 跨卷
- 卷三第 5 章 函数调用与栈
- 卷三第 8 章 闭包与逃逸
- 卷三第 14 章 错误与 panic 机制
- 卷四第 7 章 API 设计模式(函数式选项进阶)