Go 函数与错误处理
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
Go 没有异常,错误是普通的返回值。这个设计意味着:错误必须被显式处理,也意味着读代码时能一眼看到哪些地方会失败。
函数定义与多返回值
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为 0")
}
return a / b, nil
}
调用时必须处理两个返回值:
result, err := divide(10, 2)
if err != nil {
log.Printf("计算出错:%v", err)
return
}
fmt.Println(result) // 5
同名类型可以合并写 a, b float64。返回值多于两个时,建议定义一个结构体,而不是继续堆返回值。
命名返回值
func parse(raw string) (n int, err error) {
n, err = strconv.Atoi(raw)
if err != nil {
err = fmt.Errorf("解析 %q 失败: %w", raw, err)
}
return // 裸返回,返回当前 n 和 err
}
命名返回值适合需要在 defer 里修改结果的场景,但裸返回会牺牲可读性,函数超过 20 行就不建议再用。
error 是什么
type error interface {
Error() string
}
error 是一个只有一个方法的接口。任何实现了 Error() string 的类型都能当错误用:
type ConfigError struct {
Path string
}
func (e *ConfigError) Error() string {
return "配置文件不可读:" + e.Path
}
返回它时要注意:返回指针类型 &ConfigError{},不要返回值类型,否则 errors.As 匹配不到(接口里存的是值的副本)。
错误处理的三条惯例
1. 错误是最后一个返回值,nil 表示成功:
func load(path string) ([]byte, error)
2. 用 %w 包装,保留错误链:
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取配置 %s: %w", path, err)
}
%v 只保留文本,%w 保留原始错误,上层才能用 errors.Is / errors.As 判断。
3. 用 errors.Is / errors.As 判断,不要比较字符串:
if errors.Is(err, os.ErrNotExist) {
// 文件不存在
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("出错路径:", pathErr.Path)
}
errors.Is 沿错误链逐层比较(支持自定义 Is 方法),errors.As 沿链查找指定类型。直接写 err.Error() == "..." 在包装后会失效。
错误的传递与兜底
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("loadConfig: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("解析 %s: %w", path, err)
}
return &cfg, nil
}
原则:中间层只加上下文,不吞错误。真正决定「要不要继续」的是调用链顶层:
cfg, err := loadConfig("config.json")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
cfg = defaultConfig() // 可恢复:用默认值
} else {
log.Fatal(err) // 不可恢复:终止
}
}
可变参数与函数作为值
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6
nums := []int{4, 5}
sum(nums...) // 9,展开切片
函数是一等值,可以赋值、传参、返回:
type Middleware func(http.Handler) http.Handler
func apply(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
defer:延迟执行的清理
func readHead(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // 函数返回前执行,无论是否出错
buf := make([]byte, 64)
n, err := f.Read(buf)
return buf[:n], err
}
三条规则:
- 多个
defer按后进先出(LIFO)执行。 - 参数在
defer语句处立即求值,不是执行时求值:
x := 1
defer fmt.Println(x) // 打印 1,不是 2
x = 2
3 想在 defer 里读最新值,用闭包:
defer func() { fmt.Println(x) }() // 打印 2
注意:os.Exit 与 panic 之外的「函数返回」都会执行 defer,但 os.Exit 会直接终止进程,不执行任何 defer。
panic 与 recover
panic 用于程序无法继续的情况(配置缺失、不可能的状态),不是普通错误处理手段:
func mustLoad(path string) *Config {
cfg, err := loadConfig(path)
if err != nil {
panic(err) // 立即终止,打印堆栈
}
return cfg
}
recover 只能在 defer 里生效,用于进程级边界兜底(如 HTTP 中间件、任务调度器):
defer func() {
if r := recover(); r != nil {
log.Printf("捕获 panic: %v", r)
}
}()
经验法则:库代码不要 recover,把 panic 留给调用方;业务错误一律用 error 返回。
常见错误与注意事项
- 用
_丢弃错误:data, _ := os.ReadFile(p)让失败静默,至少记录日志。 - 包装时用
%v而不是%w:上层errors.Is判断失效。 - 比较错误字符串:包装后文本会变,用
errors.Is/errors.As。 - 返回错误类型值而不是指针:
errors.As匹配不到自定义类型。 defer里的参数被提前求值:需要最新值时用闭包。- 用
panic处理可恢复错误:调用方无法按类型处理,进程直接崩。 - 忘记
defer f.Close():句柄泄漏,循环里打开文件时尤其危险。
小结
- 错误是普通返回值,放最后,
nil表示成功。 - 用
fmt.Errorf("上下文: %w", err)包装,errors.Is/errors.As判断。 - 中间层只加上下文,顶层决定恢复或终止。
defer后进先出,参数立即求值;os.Exit不执行defer。panic只用于不可继续的情况,recover只放在进程边界。