Go函数必须显式返回error才能参与错误传播;应始终在函数签名中包含error、用%w包装错误、errors.Is/As判断类型、早失败快返回、不忽略Close错误。

Go 函数必须显式返回 error 才能参与错误传播
Go 没有异常机制,错误传播完全依赖函数签名是否包含 error 类型返回值。如果一个函数不声明 error 返回,调用它时就无法用 if err != nil 判断——哪怕内部 panic 了,也不会自动“冒泡”。
常见错误是把本该返回 error 的函数写成无错签名,例如:
func readFile(name string) []byte { /* 忘了返回 error */ }这导致调用方只能靠 nil 切片或空字符串猜错,丧失错误上下文。正确做法是:
- 所有可能失败的导出函数,末尾加
error返回 - 内部调用其他函数时,不忽略其
error,除非你明确决定“吞掉”并记录日志 - 不要用
panic替代error返回,除非是真正不可恢复的编程错误(如索引越界、nil 解引用)
使用 errors.Is 和 errors.As 判断错误类型,而非字符串匹配
直接比较 err == io.EOF 或 strings.Contains(err.Error(), "timeout") 是脆弱的:前者在包装后失效,后者易受翻译、格式变动影响。
立即学习“go语言免费学习笔记(深入)”;
Go 1.13+ 推荐用标准库的判断方式:
-
errors.Is(err, io.EOF)—— 检查是否为某个底层错误(支持多层包装) -
errors.As(err, &target)—— 尝试解包为具体错误类型,用于获取额外字段(如*os.PathError的Path字段) - 自定义错误应实现
Unwrap() error方法,才能被正确遍历
例如,包装一个网络错误:
type MyNetworkError struct{ Err error; Host string }
func (e *MyNetworkError) Unwrap() error { return e.Err }这样 errors.Is(wrappedErr, context.DeadlineExceeded) 才能穿透两层返回 true。
函数设计要遵循“失败早、返回快”,避免嵌套 if err != nil
Go 社区习惯把错误检查放在调用后立刻处理,而不是用大块 if/else 包裹后续逻辑。这不是风格偏好,而是为了降低认知负担和减少缩进层级。
反例(嵌套深、易漏处理):
if f, err := os.Open(name); err == nil {
if data, err := io.ReadAll(f); err == nil {
// ... 处理 data
} else {
return err
}
} else {
return err
}正例(线性、每个错误只处理一次):
f, err := os.Open(name)
if err != nil {
return fmt.Errorf("open %s: %w", name, err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("read %s: %w", name, err)
}
关键点:
- 错误处理代码紧贴调用行,视觉上不分离
- 用
%w格式动词包装错误,保留原始堆栈和类型信息 - 不要在函数中间写“先做 A 再做 B”,而要“做 A,失败就退出;否则做 B”
不要在 defer 中忽略 Close() 的错误,除非你确定它无关紧要
很多教程写 defer f.Close() 就完事,但 Close() 可能返回真实错误(比如写文件时磁盘满,Write() 成功但 Close() 失败)。忽略它等于丢掉最后的错误信号。
正确做法取决于场景:
- 如果资源关闭失败会影响结果(如写入临时文件后重命名),应在函数末尾显式检查:
if err := f.Close(); err != nil { return err } - 若已有一个主错误(如
Write()已失败),可用errors.Join()合并多个错误:return errors.Join(writeErr, f.Close()) - 仅当确认
Close()错误纯属噪音(如 HTTP 响应体读完后关 body),才可忽略,但应加注释说明原因
特别注意:os.File.Close() 在 Linux 上可能返回 EBADF,如果文件描述符已被其他 goroutine 关闭——这类竞态问题不会因忽略 Close 错误而消失,反而更难定位。










