Go错误处理哲学
Go语言采用显式错误返回机制,这与传统的异常处理有本质区别:
- 错误即值(Errors are values):错误被视为普通的值类型
- 显式处理:要求开发者必须主动检查和处理错误
- 无异常机制:不提供try-catch-finally结构
基础错误处理模式
1. 标准错误处理
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("open %s: %w", filename, err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("read %s: %w", filename, err)
}
return data, nil
}
2. 错误检查工具函数
func check(err error) {
if err != nil {
log.Fatalf("fatal error: %v", err)
}
}
// 使用示例
func main() {
f, err := os.Open("config.json")
check(err)
defer f.Close()
// ...
}
官方推荐实践
1. 错误包装(Error Wrapping)
Go 1.13+ 引入了错误包装机制:
if err != nil {
return fmt.Errorf("processing config: %w", err)
}
解包错误:
if errors.Is(err, os.ErrNotExist) {
// 处理文件不存在的特定错误
}
var pathError *os.PathError
if errors.As(err, &pathError) {
// 处理PathError类型错误
}
2. 错误定义最佳实践
自定义错误类型:
type ConfigError struct {
ConfigFile string
Err error
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("config file %s: %v", e.ConfigFile, e.Err)
}
func (e *ConfigError) Unwrap() error {
return e.Err
}
// 使用示例
func loadConfig() error {
// ...
return &ConfigError{
ConfigFile: "app.conf",
Err: os.ErrNotExist,
}
}
哨兵错误(Sentinel Errors):
var (
ErrInvalidInput = errors.New("invalid input")
ErrNotFound = errors.New("not found")
)
func Validate(input string) error {
if input == "" {
return ErrInvalidInput
}
// ...
}
高级错误处理模式
1. 错误分类处理
func HandleError(err error) {
switch {
case errors.Is(err, ErrInvalidInput):
log.Println("warning:", err)
case errors.Is(err, ErrNotFound):
log.Println("not found:", err)
createDefaultConfig()
default:
log.Println("critical error:", err)
os.Exit(1)
}
}
2. 错误上下文增强
func processRequest(req *Request) error {
if err := validateRequest(req); err != nil {
return fmt.Errorf("request %d validation failed: %w", req.ID, err)
}
// ...
}
3. 错误重试机制
func Retry(attempts int, fn func() error) error {
var err error
for i := 0; i < attempts; i++ {
if err = fn(); err == nil {
return nil
}
time.Sleep(time.Second * time.Duration(i+1))
}
return fmt.Errorf("after %d attempts: %w", attempts, err)
}
工程实践建议
1. 错误日志记录
func logError(err error, context ...interface{}) {
entry := log.WithFields(log.Fields{
"error": err.Error(),
"context": context,
"stack": string(debug.Stack()),
})
entry.Error("operation failed")
}
借助开源日志库可以快速定制化和记录错误日志。
2. HTTP错误处理
func writeError(w http.ResponseWriter, err error) {
var status int
switch {
case errors.Is(err, ErrNotFound):
status = http.StatusNotFound
case errors.Is(err, ErrInvalidInput):
status = http.StatusBadRequest
default:
status = http.StatusInternalServerError
}
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]interface{}{
"error": err.Error(),
})
}
3. 数据库错误处理
func (r *UserRepo) GetByID(id int) (*User, error) {
user := &User{}
err := r.db.Where("id = ?", id).First(user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, fmt.Errorf("database error: %w", err)
}
return user, nil
}
错误处理的“坏”例子
- 忽略错误
f, _ := os.Open("file.txt") // 没有处理打开资源时的返回错误
- 过度泛化的错误
// 不够具体
if err != nil {
return errors.New("something went wrong")
}
- 错误吞没
// 丢失原始错误信息
if err != nil {
log.Println("error occurred")
return nil
}
- 过早终止
// 可能不需要直接退出
if err != nil {
log.Fatal(err)
}
错误处理工具库推荐
- github.com/uber-go/multierr
package main
import (
"errors"
"fmt"
"go.uber.org/multierr"
)
func main() {
ExampleErrors()
}
func ExampleErrors() {
err := multierr.Combine(
nil, // successful request
errors.New("call 2 failed"),
errors.New("call 3 failed"),
)
err = multierr.Append(err, nil) // successful request
err = multierr.Append(err, errors.New("call 5 failed"))
errors := multierr.Errors(err)
for _, err := range errors {
fmt.Println(err)
}
// Output:
// call 2 failed
// call 3 failed
// call 5 failed
}
总结:优雅错误处理原则
- 透明性:错误应包含足够上下文信息
- 可追溯性:保留错误链方便调试
- 一致性:团队采用统一的错误处理风格
- 适当抽象:在适当层级处理错误
- 文档化:公开API的错误情况应有文档说明
"好的错误处理不是事后添加的功能,而是从一开始就设计的系统特性。" - Go Proverbs