Go函数必须显式返回error才能参与错误传播;应始终在函数签名中包含error、用%w包装错误、errors.Is/As判断类型、早失败快返回、不忽略Close错误。
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 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 格式动词包装错误,保留原始堆栈和类型信息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 错误而消失,反而更难定位。