版权属于:
桑帅东的博客
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
错误处理是 Go 语言的核心设计哲学之一。与传统的 try-catch 异常机制不同,Go 采用显式错误检查,强调开发者主动处理可能的失败路径。这种设计虽然提高了代码的可控性,但也对开发者提出了更高的要求。本文将深入探讨 Go 的错误处理模式,并提供实用代码示例。
一、Go 错误处理基础
错误是什么?
在 Go 中,错误是一个实现了 error 接口的值:
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都可以作为错误返回。
创建错误
基础方式:
// 使用 errors.New
import "errors"
err := errors.New("文件未找到")
// 使用 fmt.Errorf(格式化错误)
file := "config.yaml"
err := fmt.Errorf("文件 %s 不存在", file)
带错误类型的高级方式(自定义错误):
type NotFoundError struct {
File string
Line int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s:%d - 未找到", e.File, e.Line)
}
// 使用
err := &NotFoundError{"config.go", 42}
二、错误处理模式
经典检查:if err != nil
func ReadFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err // 向上传递原始错误
}
return data, nil
}
// 调用方处理
content, err := ReadFile("config.yaml")
if err != nil {
log.Printf("读取文件失败: %v", err)
return
}
错误断言(类型判断)
if nfErr, ok := err.(*NotFoundError); ok {
// 处理特定错误类型
log.Printf("文件未找到: %s", nfErr.File)
} else if os.IsNotExist(err) { // 使用标准库工具函数
log.Print("系统级文件不存在错误")
} else {
// 通用错误处理
}
三、Go 1.13+ 错误增强
错误包装(Error Wrapping)
使用 %w 占位符为错误添加上下文:
func ReadConfig() error {
_, err := os.ReadFile("config.yaml")
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
return nil
}
错误链解包
使用 errors.Is 和 errors.As 处理包装后的错误:
err := ReadConfig()
// 检查错误链中是否包含特定类型
var pathError *os.PathError
if errors.As(err, &pathError) {
fmt.Println("底层 PathError:", pathError.Path)
}
// 检查错误链中是否包含特定值
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件确实不存在")
}
四、最佳实践指南
何时返回错误?
错误处理原则
要做的:
定义哨兵错误:对重要错误定义可导出的变量:
var ErrInvalidInput = errors.New("invalid input")
避免的:
忽略错误:
data, _ = os.ReadFile("file") // 危险!
过度使用 panic:
if err != nil {
panic(err) // 仅在不可恢复时使用
}
暴露底层细节:
return fmt.Errorf("数据库错误: %v", err) // 可能泄露敏感信息
五、实战示例:分层错误处理
场景:Web 服务中的错误传递
// 数据层错误
type DatabaseError struct {
Query string
Err error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("查询 %s 失败: %v", e.Query, e.Err)
}
// 业务逻辑层
func GetUser(id int) (*User, error) {
user, err := db.QueryUser(id)
if err != nil {
return nil, &DatabaseError{
Query: fmt.Sprintf("SELECT * FROM users WHERE id = %d", id),
Err: err,
}
}
return user, nil
}
// 控制器层
func UserHandler(w http.ResponseWriter, r *http.Request) {
user, err := GetUser(123)
if err != nil {
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
http.Error(w, "数据库错误", http.StatusInternalServerError)
log.Printf("数据库错误详情: %+v", dbErr) // 记录完整信息
} else {
http.Error(w, err.Error(), http.StatusBadRequest)
}
return
}
json.NewEncoder(w).Encode(user)
}
六、错误日志记录技巧
结构化日志
import "go.uber.org/zap"
logger, _ := zap.NewProduction()
func HandleRequest() {
err := processRequest()
if err != nil {
logger.Error("请求处理失败",
zap.Error(err), // 记录错误对象
zap.String("path", "/api"), // 添加上下文
zap.Int("status", 500),
)
}
}
堆栈跟踪记录
使用第三方库增强错误信息:
import "github.com/pkg/errors"
func ReadConfig() error {
_, err := os.ReadFile("config.yaml")
if err != nil {
return errors.Wrap(err, "读取配置文件失败")
}
return nil
}
// 记录错误时打印堆栈
fmt.Printf("%+v\n", err)
七、panic 与 recover
使用准则:
recover:在顶层函数(如 HTTP 中间件)中使用
示例:Web 服务恢复中间件
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("Recovered from panic: %v", err)
http.Error(w, "服务器内部错误", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
评论