应使用 errors.Wrap 或 fmt.Errorf(%w) 封装错误以保留上下文和调用栈,定义自定义错误类型替代字符串判断,中间件统一处理可预期错误,defer 中需显式处理并组合关闭错误。

用 errors.Wrap 或 fmt.Errorf 带上下文封装错误
Go 原生错误对象本身不带调用栈或上下文,直接返回 err 会让上层无法判断出错位置和原因。常见错误是写成 return err 就完事,结果日志里只看到 "no such file or directory",却不知道是哪个配置文件、在哪个函数里打开失败。
推荐统一用 github.com/pkg/errors 的 Wrap,或 Go 1.13+ 原生 fmt.Errorf 的 %w 动词:
if _, err := os.Open(path); err != nil {
return errors.Wrap(err, "failed to open config file") // 带上下文
// 或:return fmt.Errorf("failed to open config file: %w", err)
}
这样后续用 errors.Is 或 errors.As 仍可识别原始错误类型,同时日志/调试时能看清完整路径。
定义业务错误类型而非硬编码字符串
用字符串判断错误(如 if strings.Contains(err.Error(), "timeout"))脆弱且难维护。应为关键业务场景定义自定义错误类型,实现 Is 和 Unwrap 方法。
立即学习“go语言免费学习笔记(深入)”;
例如网络重试失败场景:
type TimeoutError struct {
Op string
Err error
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("timeout on %s: %v", e.Op, e.Err)
}
func (e *TimeoutError) Unwrap() error { return e.Err }
func (e *TimeoutError) Is(target error) bool {
_, ok := target.(*TimeoutError)
return ok
}
使用时:return &TimeoutError{Op: "fetch-user", Err: ctx.Err()};捕获时:if errors.Is(err, &TimeoutError{}) —— 类型安全、可测试、易扩展。
在中间件或公共函数中统一处理可恢复错误
不是所有错误都要层层往上抛。比如 HTTP handler 中的验证失败、参数解析错误,适合在入口处拦截并转成标准响应,避免每个 handler 都写重复的 if err != nil { writeJSONError(...) }。
常见做法是用闭包封装 handler:
func withErrorHandling(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
switch {
case errors.Is(err, ErrValidationFailed):
http.Error(w, "bad request", http.StatusBadRequest)
case errors.Is(err, ErrNotFound):
http.Error(w, "not found", http.StatusNotFound)
default:
log.Printf("unexpected error: %+v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
}
}
注册路由时:http.HandleFunc("/user", withErrorHandling(handleUser))。注意:仅对「已分类、可预期」的错误做这种集中处理,别把所有 error 都吞掉。
避免在 defer 中忽略错误或覆盖已有错误
典型反模式:defer f.Close() —— 如果 f.Close() 返回非 nil 错误,它会被丢弃;更糟的是,在函数末尾已有错误时又执行 Close() 并覆盖原错误。
正确做法是显式检查并组合:
- 若关闭失败且主逻辑无错,就返回关闭错误
- 若主逻辑已出错,用
errors.Wrap把关闭错误作为补充信息附上
例如:
func processFile(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
if err == nil {
err = closeErr
} else {
err = errors.Wrap(err, "and also failed to close file")
}
}
}()
// ... use f
return nil
}
这个细节容易被忽略,但线上排查时,一个被吞掉的 close 错误可能掩盖真正的资源泄漏问题。










