Error 处理模式
Go 的错误处理是其最具争议的设计之一,但也是最务实的。掌握正确的错误处理模式,是写出高质量 Go 代码的关键。
基础:error 接口
go
// error 是内置接口
type error interface {
Error() string
}
// 最简单的错误创建
err1 := errors.New("something went wrong")
err2 := fmt.Errorf("user %d not found", userID)errors 包核心 API(Go 1.13+)
go
import "errors"
// errors.New — 创建简单错误
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
// fmt.Errorf + %w — 包装错误(保留原始错误)
func getUser(id int) (*User, error) {
user, err := db.Query(id)
if err != nil {
return nil, fmt.Errorf("getUser(%d): %w", id, err)
}
return user, nil
}
// errors.Is — 检查错误链中是否包含目标错误
err := getUser(42)
if errors.Is(err, ErrNotFound) {
// 处理未找到的情况
}
// errors.As — 提取错误链中的具体类型
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
fmt.Println("数据库错误码:", dbErr.Code)
}
// errors.Unwrap — 获取被包装的错误
wrapped := fmt.Errorf("outer: %w", ErrNotFound)
inner := errors.Unwrap(wrapped) // ErrNotFound自定义错误类型
go
// 携带上下文信息的错误
type AppError struct {
Code int
Message string
Err error // 原始错误
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// 实现 Unwrap 支持 errors.Is/As 链式查找
func (e *AppError) Unwrap() error {
return e.Err
}
// 错误码常量
const (
ErrCodeNotFound = 404
ErrCodeForbidden = 403
ErrCodeInternal = 500
)
func NewNotFoundError(resource string, id any) *AppError {
return &AppError{
Code: ErrCodeNotFound,
Message: fmt.Sprintf("%s(%v) not found", resource, id),
}
}错误包装最佳实践
go
// 在调用链中添加上下文,但不要重复
// 好的做法
func (s *UserService) GetProfile(userID int) (*Profile, error) {
user, err := s.repo.FindUser(userID)
if err != nil {
return nil, fmt.Errorf("UserService.GetProfile: %w", err)
}
profile, err := s.repo.FindProfile(user.ProfileID)
if err != nil {
return nil, fmt.Errorf("UserService.GetProfile: %w", err)
}
return profile, nil
}
// 错误链示例:
// UserService.GetProfile: Repository.FindUser: sql: no rows in result set
// 清晰地展示了调用路径Sentinel 错误(哨兵错误)
go
// 包级别的预定义错误,用于比较
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidPassword = errors.New("invalid password")
ErrAccountLocked = errors.New("account locked")
)
func Login(username, password string) (*User, error) {
user, err := findUser(username)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("login: %w", err)
}
if !checkPassword(user, password) {
return nil, ErrInvalidPassword
}
return user, nil
}
// 调用方
user, err := Login("alice", "wrong")
switch {
case errors.Is(err, ErrUserNotFound):
http.Error(w, "用户不存在", 404)
case errors.Is(err, ErrInvalidPassword):
http.Error(w, "密码错误", 401)
case err != nil:
http.Error(w, "服务器错误", 500)
}panic 与 recover
go
// panic 用于不可恢复的错误(程序 bug,不是业务错误)
func divide(a, b int) int {
if b == 0 {
panic("division by zero") // 程序 bug
}
return a / b
}
// recover 只在 defer 中有效
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered panic: %v", r)
}
}()
return divide(a, b), nil
}
// Web 框架中间件:防止 panic 导致服务崩溃
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// 记录堆栈
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
log.Printf("panic: %v\n%s", err, buf[:n])
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}多错误处理(Go 1.20+)
go
// errors.Join — 合并多个错误
func validateUser(u User) error {
var errs []error
if u.Name == "" {
errs = append(errs, errors.New("name is required"))
}
if u.Age < 0 || u.Age > 150 {
errs = append(errs, fmt.Errorf("invalid age: %d", u.Age))
}
if !isValidEmail(u.Email) {
errs = append(errs, fmt.Errorf("invalid email: %s", u.Email))
}
return errors.Join(errs...) // nil if errs is empty
}
err := validateUser(User{Age: -1, Email: "bad"})
// err.Error() = "name is required\ninvalid age: -1\ninvalid email: bad"实战:HTTP 错误处理
go
// 统一的 HTTP 错误响应
type HTTPError struct {
StatusCode int `json:"-"`
Code string `json:"code"`
Message string `json:"message"`
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
var (
ErrHTTPNotFound = &HTTPError{404, "NOT_FOUND", "资源不存在"}
ErrHTTPForbidden = &HTTPError{403, "FORBIDDEN", "无权访问"}
ErrHTTPBadRequest = &HTTPError{400, "BAD_REQUEST", "请求参数错误"}
)
// 错误处理中间件
func errorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 使用 panic 传递错误(某些框架的做法)
defer func() {
if err := recover(); err != nil {
var httpErr *HTTPError
switch e := err.(type) {
case *HTTPError:
httpErr = e
case error:
httpErr = &HTTPError{500, "INTERNAL_ERROR", e.Error()}
default:
httpErr = &HTTPError{500, "INTERNAL_ERROR", "未知错误"}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpErr.StatusCode)
json.NewEncoder(w).Encode(httpErr)
}
}()
next.ServeHTTP(w, r)
})
}错误处理的 Go 哲学
go
// 1. 错误是值,不是异常
// 显式处理每个可能的错误
// 2. 不要忽略错误
f, err := os.Open("file.txt")
if err != nil {
return err // 不要 _ = err
}
// 3. 只包装一次,不要重复包装
// 坏:
return fmt.Errorf("error: %w", fmt.Errorf("error: %w", err))
// 好:
return fmt.Errorf("processFile: %w", err)
// 4. 在包边界转换错误类型
// 内部实现细节不应该泄漏到公共 API
// 5. 使用 log/slog 记录错误上下文
slog.Error("处理请求失败",
"error", err,
"user_id", userID,
"path", r.URL.Path,
)推荐工具库
github.com/pkg/errors— 带堆栈的错误包装(历史库,现在标准库已够用)github.com/cockroachdb/errors— 更强大的错误处理go.uber.org/multierr— 多错误合并(Go 1.20 前的替代方案)