Go错误处理的设计哲学与基本模式
Go语言的错误处理采用显式返回值方式,函数通过error接口向调用方传递错误信息。与异常机制不同,Go要求开发者在每一步调用点处理或转发错误,虽然代码看起来冗长,但错误路径完全可见、可追踪、可控。
Go 1.13引入的错误链(error chain)机制和errors.Is/As函数,以及fmt.Errorf的%w动词,让错误处理从简单的字符串比较进化为结构化的错误链路追踪。这为自定义错误类型和错误包装提供了标准化的设计范式。
// 基本错误创建与比较
var ErrNotFound = errors.New("resource not found")
func GetUser(id int) (*User, error) {
row := db.QueryRow("SELECT ...", id)
var u User
if err := row.Scan(&u.Name, &u.Email); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("get user %d: %w", id, ErrNotFound)
}
return nil, fmt.Errorf("get user %d: %w", id, err)
}
return &u, nil
}
自定义错误类型与结构化错误信息
当错误需要携带上下文信息(错误码、模块名、原始错误等)时,定义自定义错误类型是标准做法。自定义错误类型实现error接口,同时提供额外的类型化访问方法。
// 自定义错误类型
type AppError struct {
Code string // 业务错误码,如 "USER_001"
Message string // 用户友好的错误描述
Detail string // 内部调试信息
Err error // 原始错误
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 工厂函数
func NewAppError(code, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
自定义错误类型的设计原则:Code字段使用常量而非magic string;Message面向最终用户,不含技术细节;Detail字段仅在日志中输出,不返回给客户端;Unwrap方法支持errors.Is/As的链式查找。
错误包装与errors.Is/As链式查找
fmt.Errorf配合%w动词创建包装错误,形成错误链。errors.Is沿链路递归查找目标错误,errors.As查找链路中特定类型的错误值:
func ProcessOrder(orderID string) error {
inventory, err := checkInventory(orderID)
if err != nil {
return fmt.Errorf("process order %s: %w", orderID, err)
}
if err := reserveStock(inventory); err != nil {
return fmt.Errorf("process order %s: %w", orderID, err)
}
if err := createPayment(orderID); err != nil {
// 多层包装
return fmt.Errorf("process order %s: %w", orderID,
NewAppError("PAY_001", "payment failed", err))
}
return nil
}
// 调用方使用errors.Is/As做精确匹配
func HandleError(err error) {
if errors.Is(err, ErrNotFound) {
respondNotFound()
return
}
var appErr *AppError
if errors.As(err, &appErr) {
respondWithCode(appErr.Code, appErr.Message)
log.Printf("detail: %s", appErr.Detail)
return
}
respondInternalError()
}
错误分组与sentinel错误设计
大型项目中错误码数量膨胀后,需要按模块组织sentinel错误。推荐按包/模块维度定义错误变量,用errors.Is实现分组判断:
// errors/user.go
var (
ErrUserNotFound = errors.New("user not found")
ErrUserExists = errors.New("user already exists")
ErrUserDisabled = errors.New("user disabled")
)
// errors/order.go
var (
ErrOrderInvalid = errors.New("order invalid")
ErrOrderExpired = errors.New("order expired")
ErrOrderPaid = errors.New("order already paid")
)
// errors/group.go - 错误分组
func IsUserError(err error) bool {
return errors.Is(err, ErrUserNotFound) ||
errors.Is(err, ErrUserExists) ||
errors.Is(err, ErrUserDisabled)
}
func IsOrderError(err error) bool {
return errors.Is(err, ErrOrderInvalid) ||
errors.Is(err, ErrOrderExpired) ||
errors.Is(err, ErrOrderPaid)
}
HTTP层可根据错误分组统一设置响应状态码:用户类错误返回400,订单类错误根据具体类型返回400或409。
错误处理中间件与日志链路追踪
在HTTP服务中,统一错误处理中间件是标准做法。中间件捕获handler返回的错误,根据类型生成HTTP响应,同时记录结构化日志:
func ErrorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var handler func(http.ResponseWriter, *http.Request) error
err := handler(w, r)
if err == nil {
return
}
var appErr *AppError
if errors.As(err, &appErr) {
status := mapCodeToStatus(appErr.Code)
respondJSON(w, status, map[string]string{
"code": appErr.Code,
"message": appErr.Message,
})
slog.Error("request failed",
"method", r.Method,
"path", r.URL.Path,
"code", appErr.Code,
"error", appErr.Err,
)
return
}
respondJSON(w, 500, map[string]string{
"code": "INTERNAL",
"message": "internal server error",
})
slog.Error("unhandled error",
"method", r.Method,
"path", r.URL.Path,
"error", err,
)
})
}
slog结构化日志配合错误链追踪,可在日志系统中通过error code快速筛选和聚合同类错误,比传统的字符串匹配更精确。
Go 1.13之后的错误处理体系已经相当完善。关键原则:错误在创建时包装上下文,在处理时用Is/As做类型化判断,在日志中记录完整链路。避免在中间层吞掉错误(只记日志不返回),避免用字符串匹配判断错误类型,避免在error message中拼接敏感信息。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-cuo-wu-chu-li-lian-yu-zi-ding-yi-cuo-wu-lei-xing/