自定义 error 类型必须实现 error() 方法,接收者需为指针类型且返回 string;应使用 errors.as 判断错误类型;中间件需分别处理 panic 和显式 error;错误包装必须用 %w,敏感信息须结构化记录。

自定义 error 类型必须实现 Error() 方法
不实现这个方法,结构体就不是合法的 error,if err != nil 会永远为 false,fmt.Println(err) 打印出来是 &{...} 这种内存地址,日志里完全不可读。
常见错误包括:
- 接收者用了值类型
func (e MyError) Error() string—— 修改字段不生效,且嵌套包装后可能丢失原始值 - 返回类型写成
int或error而非string - 字段私有(如
message string)但Error()里又没提供公开访问方式,外部构造后连消息都取不到
✅ 正确写法示例:
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
用 errors.As 判断类型,别用类型断言
业务中常要区分“参数错”和“数据库超时”,靠 err == ErrInvalidInput 或 err.(*AppError) 都会失效——前者只对变量地址有效,后者在被 fmt.Errorf("xxx: %w", err) 包装一层后直接 panic。
errors.As 是唯一能穿透任意层包装、安全提取目标错误类型的手段。
- 第二个参数必须是指针变量,不是类型字面量:
var e *AppError; if errors.As(err, &e) { ... } - 不能写成
if errors.As(err, (*AppError)(nil)),这会 panic - 如果想判断是否是某类错误(比如所有 400 级),建议统一继承一个接口,而非硬编码类型名
中间件里统一捕获 panic 和显式 error
HTTP handler 里 panic 不处理会直接终止请求,显式返回 error 又容易漏判。两件事得拆成两个中间件做:
-
Recovery:用defer recover()捕获 panic,转成&AppError{Code: 500, Message: "server error"},并调用debug.Stack()记录完整堆栈 -
ErrorHandler:从context.Context中取预先注入的 error(比如通过ctx.Value(middleware.ErrorKey)),或约定 handler 最后返回error并由中间件统一读取 - 两者都要确保在出错后调用
c.Abort()(Gin)或http.Error,避免后续中间件继续执行
⚠️ 注意:recover() 后必须清空 panic 状态,否则同 goroutine 内后续 recover() 会失效。
错误包装必须用 %w,否则监控链就断了
日志系统、APM 工具(如 Sentry、Datadog)依赖错误链还原调用路径。用 %s 或 + 拼接会彻底丢掉原始错误,errors.Is 和 errors.As 全部失效。
- 底层 DB 层报错 → service 层加操作上下文 → handler 层加 traceID,每层都用
fmt.Errorf("update user: %w", err) - 敏感信息(如用户 ID、token)绝不能塞进错误字符串,要用结构化字段单独记录:
log.Errorw("db query failed", "trace_id", tid, "user_id", uid, "error", err) - 第三方库错误(如
sql.ErrNoRows、pgx.PgError)要在你自己的 DAO 层做一次映射,转成统一*AppError,别让上层直接面对底层细节
真正难的不是写一个 AppError,而是让所有人——包括新来的同事——在每一层都坚持用 %w 包装、用 errors.As 提取、不在日志里拼接敏感字段。这些点一旦松动,全局监控就只剩 HTTP 状态码和模糊字符串。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











