Go错误处理模式
约 1603 字大约 5 分钟
goerror
2025-04-22
概述
Go采用了显式的错误处理机制——函数通过返回 error 接口值来表示错误状态,调用方必须显式检查。这种设计虽然导致了大量的 if err != nil 代码,但使得错误处理路径清晰可见,便于维护和调试。本文将系统介绍Go的错误处理最佳实践,包括哨兵错误、错误包装、自定义错误类型等模式。
error 接口
// Go内置的error接口极其简单
type error interface {
Error() string
}
// 最常用的创建方式
err := errors.New("something went wrong")
err := fmt.Errorf("failed to open %s: %w", filename, cause)哨兵错误(Sentinel Errors)
预定义的错误值,用于表示特定的错误条件:
// 标准库中的哨兵错误
var (
io.EOF = errors.New("EOF")
sql.ErrNoRows = errors.New("sql: no rows in result set")
os.ErrNotExist = errors.New("file does not exist")
context.Canceled = errors.New("context canceled")
)
// 使用
data, err := io.ReadAll(r)
if err != nil {
if errors.Is(err, io.EOF) {
// 正常结束
return data, nil
}
return nil, err
}
// 自定义哨兵错误
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)注意:不要用 == 比较错误,使用 errors.Is(),因为错误可能被包装。
错误包装(Error Wrapping)
Go 1.13引入了错误包装机制:
// 使用 %w 包装错误(创建因果链)
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("readConfig: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("readConfig: parse %s: %w", path, err)
}
return &cfg, nil
}errors.Is 和 errors.As
// errors.Is: 沿着包装链查找特定的错误值
err := fmt.Errorf("operation failed: %w", os.ErrNotExist)
fmt.Println(errors.Is(err, os.ErrNotExist)) // true
// errors.As: 沿着包装链查找特定的错误类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("path:", pathErr.Path)
fmt.Println("op:", pathErr.Op)
}多重包装(Go 1.20+)
// Go 1.20支持同时包装多个错误
err := fmt.Errorf("multiple causes: %w and %w", err1, err2)
// 或使用errors.Join
err := errors.Join(err1, err2, err3)
// errors.Is会检查所有包装的错误
errors.Is(err, err1) // true
errors.Is(err, err2) // true自定义错误类型
// 携带结构化信息的错误
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed: field %s (%v): %s",
e.Field, e.Value, e.Message)
}
// 支持包装的自定义错误
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)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 使用
func getUser(id int) (*User, error) {
user, err := db.QueryUser(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, &AppError{
Code: 404,
Message: "user not found",
Err: err,
}
}
return nil, &AppError{
Code: 500,
Message: "database error",
Err: err,
}
}
return user, nil
}错误处理策略
策略一:向上传播(最常见)
func processOrder(orderID string) error {
order, err := fetchOrder(orderID)
if err != nil {
return fmt.Errorf("processOrder %s: %w", orderID, err)
}
// ...
return nil
}策略二:处理并恢复
func loadConfig(path string) *Config {
cfg, err := readConfigFile(path)
if err != nil {
log.Printf("failed to load config from %s: %v, using defaults", path, err)
return defaultConfig()
}
return cfg
}策略三:重试
func fetchWithRetry(url string, maxRetries int) ([]byte, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
data, err := fetch(url)
if err == nil {
return data, nil
}
lastErr = err
// 判断是否可重试
if !isRetryable(err) {
return nil, fmt.Errorf("non-retryable error: %w", err)
}
backoff := time.Duration(1<<uint(i)) * 100 * time.Millisecond
time.Sleep(backoff)
}
return nil, fmt.Errorf("after %d retries: %w", maxRetries, lastErr)
}
func isRetryable(err error) bool {
var netErr net.Error
if errors.As(err, &netErr) {
return netErr.Timeout()
}
return false
}策略四:降级
func getPrice(productID string) (float64, error) {
// 尝试从缓存获取
price, err := cache.Get(productID)
if err == nil {
return price, nil
}
// 缓存失败,从数据库获取
price, err = db.GetPrice(productID)
if err != nil {
// 数据库也失败,使用默认价格
log.Printf("failed to get price for %s: %v, using default", productID, err)
return defaultPrice, nil
}
return price, nil
}panic 与 recover
// panic应该只用于不可恢复的错误
// 例如:程序初始化失败、不可能发生的状态
// HTTP服务器中的recover中间件
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic recovered: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}与异常机制的对比
// Go的错误处理 vs 异常处理
// Java/Python/C++: 异常
// try {
// result = riskyOperation();
// process(result);
// } catch (IOException e) {
// handleIO(e);
// } catch (Exception e) {
// handleGeneral(e);
// }
// Go: 显式错误
result, err := riskyOperation()
if err != nil {
var ioErr *IOError
if errors.As(err, &ioErr) {
return handleIO(ioErr)
}
return handleGeneral(err)
}
process(result)
// Go的优势:
// 1. 错误路径可见,不会被隐式忽略
// 2. 没有异常表开销
// 3. 不需要考虑异常安全性(RAII等)
// 4. 函数签名明确表达可能的错误
// Go的劣势:
// 1. 代码冗长(if err != nil 重复)
// 2. 容易忘记检查错误(_ = doSomething())实用工具函数
// 1. must模式(仅用于初始化,失败即panic)
func must[T any](val T, err error) T {
if err != nil {
panic(err)
}
return val
}
var tmpl = must(template.ParseFiles("layout.html"))
// 2. 错误聚合
type MultiError struct {
errs []error
}
func (me *MultiError) Add(err error) {
if err != nil {
me.errs = append(me.errs, err)
}
}
func (me *MultiError) Err() error {
if len(me.errs) == 0 {
return nil
}
return errors.Join(me.errs...)
}
// 3. 延迟错误处理
func closeWithError(c io.Closer, err *error) {
if cerr := c.Close(); cerr != nil && *err == nil {
*err = cerr
}
}
func readFile(path string) (data []byte, err error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer closeWithError(f, &err)
return io.ReadAll(f)
}总结
| 模式 | 用途 | 示例 |
|---|---|---|
| 哨兵错误 | 可预期的固定错误 | io.EOF, sql.ErrNoRows |
| 错误包装 | 添加上下文信息 | fmt.Errorf("%w", err) |
| 自定义类型 | 携带结构化信息 | ValidationError |
| errors.Is | 检查错误链中的值 | errors.Is(err, ErrNotFound) |
| errors.As | 检查错误链中的类型 | errors.As(err, &pathErr) |
| errors.Join | 聚合多个错误 | errors.Join(err1, err2) |
Go的错误处理设计哲学是"错误是值"。通过将错误作为普通值来处理,Go提供了灵活而强大的错误处理能力。遵循"包装传播、在边界处理"的原则,可以构建出清晰可维护的错误处理体系。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于