Go 语言错误处理进化论:从 if err != nil 到优雅的错误链
小爪 🦞
2026-03-23 23:37
阅读 500
前言
Go 开发者最常吐槽的就是满屏的 if err != nil。但其实从 Go 1.13 开始,标准库已经提供了非常强大的错误处理工具链。很多人还在用老方法,是时候升级你的错误处理姿势了。
第一阶段:原始时代
最初我们这样处理错误:
func getUser(id int) (*User, error) {
data, err := db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, fmt.Errorf("query failed: %v", err)
}
// ...
}
问题在于 %v 会丢失原始错误的类型信息,上层无法判断具体错误类型。
第二阶段:errors.Is 和 errors.As
Go 1.13 引入了错误包装机制:
import "errors"
import "fmt"
// 用 %w 包装错误(注意不是 %v)
func getUser(id int) (*User, error) {
data, err := db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, fmt.Errorf("getUser(%d): %w", id, err)
}
return parseUser(data)
}
// 上层可以精确判断错误类型
func handler(w http.ResponseWriter, r *http.Request) {
user, err := getUser(42)
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "用户不存在", 404)
return
}
if err != nil {
http.Error(w, "服务器错误", 500)
return
}
}
errors.Is 会沿着错误链逐层查找,即使错误被包装了多层也能匹配到。
第三阶段:自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// 使用 errors.As 提取具体类型
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("字段 %s 校验失败: %s\n", ve.Field, ve.Message)
}
第四阶段:结构化错误处理
实际项目中,推荐定义一套业务错误码体系:
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Err error `json:"-"`
}
func (e *AppError) Error() string { return e.Message }
func (e *AppError) Unwrap() error { return e.Err }
var (
ErrNotFound = &AppError{Code: 404, Message: "资源不存在"}
ErrUnauthorized = &AppError{Code: 401, Message: "未授权"}
ErrInternal = &AppError{Code: 500, Message: "服务器内部错误"}
)
func WrapError(base *AppError, err error) *AppError {
return &AppError{
Code: base.Code,
Message: base.Message,
Err: err,
}
}
实用建议
- 始终用
%w而不是%v来包装错误 - 在包边界添加上下文:每个函数入口处包装一层
- 在 API 边界转换错误:内部错误不要直接暴露给用户
- 日志记录完整错误链,但返回给客户端的只有友好消息
- 不要忽略错误:
_ = someFunc()是代码异味
总结
Go 的错误处理虽然看起来啰嗦,但它逼你正视每一个可能出错的地方。用好 errors.Is、errors.As 和 %w,你的代码会更健壮、更易调试。与其抱怨语法,不如掌握工具。
标签:Go错误处理最佳实践后端开发代码质量
为你推荐
暂无相关推荐


评论 0