Go 错误处理最佳实践
错误处理是 Go 语言最显著的特征之一。与传统的异常机制不同,Go 采用显式的错误返回值模式。本文将深入探讨 Go 的错误处理机制,分享最佳实践和常见陷阱。
一、错误处理基础
1.1 错误返回值模式
Go 使用多返回值机制返回错误:
// 标准模式
file, err := os.Open("config.json")
if err != nil {
// 处理错误
log.Printf("打开文件失败:%v", err)
return err
}
defer file.Close()
// 处理成功逻辑
data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("读取文件失败:%w", err)
}
1.2 error 接口
Go 的 error 是一个内置接口:
type error interface {
Error() string
}
// 简单错误
err := errors.New("something went wrong")
fmt.Println(err.Error()) // something went wrong
// 格式化错误
err := fmt.Errorf("用户 %s 不存在", "alice")
fmt.Println(err.Error()) // 用户 alice 不存在
1.3 错误处理原则
// ✅ 正确处理错误
result, err := doSomething()
if err != nil {
return err // 或记录日志、重试等
}
use(result)
// ❌ 忽略错误(除非明确不需要)
result, _ := doSomething() // 危险!
use(result)
// ✅ 明确忽略(使用空白标识符)
_, err = file.WriteString("data")
if err != nil {
log.Printf("写入失败:%v", err)
}
// 或者
_ = file.Close() // 明确忽略
二、错误包装(Go 1.13+)
2.1 为什么要包装错误
错误包装可以:
- 添加上下文信息
- 保留原始错误
- 便于错误追踪
// 不包装:丢失上下文
func loadConfig() error {
data, err := os.ReadFile("config.json")
if err != nil {
return err // 调用者不知道是在加载配置时出错
}
// ...
return nil
}
// 包装:添加上下文
func loadConfig() error {
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("加载配置失败:%w", err)
}
// ...
return nil
}
2.2 错误包装语法
// 使用 %w 包装错误(只能包装一个)
err := fmt.Errorf("连接数据库失败:%w", originalErr)
// 使用 %v 或 %s 不包装(仅显示)
err := fmt.Errorf("操作失败:%v", originalErr) // 不保留原始错误
// 多层包装
err := fmt.Errorf("第 1 层:%w",
fmt.Errorf("第 2 层:%w",
fmt.Errorf("第 3 层:%v", baseErr)))
2.3 错误解包
Go 1.13 提供了 errors.Is 和 errors.As:
// 检查错误类型
if errors.Is(err, os.ErrNotExist) {
log.Println("文件不存在,使用默认配置")
}
// 提取自定义错误类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Printf("路径错误:%s", pathErr.Path)
}
// 等价于旧写法(不推荐)
if err == os.ErrNotExist {
// ...
}
2.4 自定义可解包错误
type AppError struct {
Code string
Message string
Err error // 内部错误
}
// 实现 Error 方法
func (e *AppError) Error() string {
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// 实现 Unwrap 方法(Go 1.13+)
func (e *AppError) Unwrap() error {
return e.Err
}
// 使用
func getUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, &AppError{
Code: "USER_NOT_FOUND",
Message: fmt.Sprintf("用户 %s 不存在", id),
Err: err,
}
}
return user, nil
}
// 检查
_, err := getUser("123")
var appErr *AppError
if errors.As(err, &appErr) {
if appErr.Code == "USER_NOT_FOUND" {
// 处理用户不存在
}
}
三、自定义错误类型
3.1 简单错误类型
// 定义错误类型
type ValidationError struct {
Field string
Message string
}
// 实现 error 接口
func (e *ValidationError) Error() string {
return fmt.Sprintf("验证失败 [%s]: %s", e.Field, e.Message)
}
// 使用
func validateEmail(email string) error {
if !strings.Contains(email, "@") {
return &ValidationError{
Field: "email",
Message: "必须包含 @ 符号",
}
}
return nil
}
// 处理
if err := validateEmail("invalid"); err != nil {
var vErr *ValidationError
if errors.As(err, &vErr) {
log.Printf("字段 %s 验证失败:%s", vErr.Field, vErr.Message)
}
}
3.2 错误哨兵(Sentinel Errors)
预定义的错误值,用于精确匹配:
// 定义包级别的错误
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
// 使用
func getUser(id string) (*User, error) {
if id == "" {
return nil, ErrInvalidInput
}
// ...
if !exists {
return nil, ErrNotFound
}
return user, nil
}
// 检查
user, err := getUser("")
if errors.Is(err, ErrNotFound) {
// 处理未找到
} else if errors.Is(err, ErrInvalidInput) {
// 处理无效输入
}
3.3 错误工厂函数
// 错误工厂
func NewNotFoundError(resource, id string) error {
return &NotFoundError{
Resource: resource,
ID: id,
}
}
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %s not found", e.Resource, e.ID)
}
// 使用
err := NewNotFoundError("user", "123")
// 输出:user 123 not found
四、错误处理模式
4.1 错误转换
在不同层之间转换错误类型:
// DAO 层
func (d *UserDAO) FindByID(id string) (*User, error) {
row := d.db.QueryRow("SELECT * FROM users WHERE id = ?", id)
if row.Err() != nil {
if errors.Is(row.Err(), sql.ErrNoRows) {
return nil, domain.ErrUserNotFound
}
return nil, fmt.Errorf("数据库查询失败:%w", row.Err())
}
// ...
}
// Service 层
func (s *UserService) GetUser(id string) (*UserDTO, error) {
user, err := s.userDAO.FindByID(id)
if err != nil {
if errors.Is(err, domain.ErrUserNotFound) {
return nil, apperror.NewNotFound("用户", id)
}
return nil, apperror.NewInternal("获取用户失败", err)
}
// ...
}
// Handler 层
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
user, err := h.userService.GetUser(id)
if err != nil {
var appErr *apperror.AppError
if errors.As(err, &appErr) {
switch appErr.Type {
case apperror.TypeNotFound:
http.Error(w, appErr.Message, http.StatusNotFound)
case apperror.TypeInternal:
http.Error(w, "服务器错误", http.StatusInternalServerError)
}
}
return
}
json.NewEncoder(w).Encode(user)
}
4.2 错误聚合
收集多个错误:
type MultiError []error
func (m MultiError) Error() string {
if len(m) == 0 {
return ""
}
msgs := make([]string, len(m))
for i, err := range m {
msgs[i] = err.Error()
}
return strings.Join(msgs, "; ")
}
// 使用
func validateUser(user *User) error {
var errs MultiError
if user.Name == "" {
errs = append(errs, errors.New("姓名不能为空"))
}
if user.Age < 0 {
errs = append(errs, errors.New("年龄不能为负"))
}
if user.Email == "" {
errs = append(errs, errors.New("邮箱不能为空"))
}
if len(errs) > 0 {
return errs
}
return nil
}
4.3 重试错误处理
type RetryableError struct {
Err error
}
func (e *RetryableError) Error() string {
return fmt.Sprintf("可重试错误:%v", e.Err)
}
func (e *RetryableError) Unwrap() error {
return e.Err
}
// 重试函数
func doWithRetry(fn func() error, maxRetries int) error {
var err error
for i := 0; i < maxRetries; i++ {
err = fn()
if err == nil {
return nil
}
// 检查是否可重试
var retryErr *RetryableError
if !errors.As(err, &retryErr) {
return err // 不可重试,直接返回
}
log.Printf("重试 %d/%d: %v", i+1, maxRetries, err)
time.Sleep(time.Second * time.Duration(i+1))
}
return err
}
// 使用
err := doWithRetry(func() error {
resp, err := http.Get("https://api.example.com/data")
if err != nil {
return &RetryableError{Err: err}
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return &RetryableError{
Err: fmt.Errorf("服务器错误:%d", resp.StatusCode),
}
}
return nil
}, 3)
4.4 延迟错误处理
// 使用 defer 统一处理错误
func processFile(filename string) (err error) {
file, err := os.Open(filename)
if err != nil {
return err
}
defer func() {
if closeErr := file.Close(); closeErr != nil {
err = fmt.Errorf("关闭文件失败:%w", closeErr)
}
}()
// 处理逻辑
data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("读取文件失败:%w", err)
}
return process(data)
}
五、panic 与 recover
5.1 何时使用 panic
使用场景:
- 不可恢复的错误
func init() {
if config == nil {
panic("配置未初始化")
}
}
- 编程错误
func MustParse(s string) *Config {
cfg, err := Parse(s)
if err != nil {
panic(err) // 调用者的错误
}
return cfg
}
- 开发阶段检查
func process(items []Item) {
if len(items) == 0 {
panic("items 不能为空")
}
// ...
}
5.2 recover 使用
// 恢复 panic
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("执行失败:%v", r)
}
}()
fn()
return nil
}
// HTTP 中间件
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic recovered: %v", r)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
5.3 不要滥用 recover
// ❌ 不推荐:掩盖所有错误
func doSomething() {
defer func() {
recover() // 吞掉所有 panic
}()
// 危险代码
}
// ✅ 推荐:记录并重新抛出
func doSomething() (err error) {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic: %v", r)
err = fmt.Errorf("内部错误:%v", r)
}
}()
// 正常逻辑
}
六、错误日志记录
6.1 结构化日志
type Logger interface {
Error(msg string, keysAndValues ...interface{})
}
func processOrder(orderID string) error {
order, err := db.GetOrder(orderID)
if err != nil {
log.Error("获取订单失败",
"order_id", orderID,
"error", err,
)
return fmt.Errorf("获取订单失败:%w", err)
}
// ...
}
6.2 错误堆栈追踪
// 使用第三方库记录堆栈
import "github.com/pkg/errors"
func process() error {
_, err := os.Open("notexist.txt")
if err != nil {
return errors.WithStack(err) // 添加堆栈
}
// ...
}
// 输出完整堆栈
func main() {
if err := process(); err != nil {
log.Printf("%+v", err) // %+v 打印堆栈
}
}
6.3 错误分级
type ErrorLevel int
const (
LevelDebug ErrorLevel = iota
LevelInfo
LevelWarn
LevelError
LevelFatal
)
type AppError struct {
Level ErrorLevel
Code string
Message string
Err error
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}
// 使用
func handler(w http.ResponseWriter, r *http.Request) {
err := doSomething()
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
switch appErr.Level {
case LevelDebug:
log.Debug(appErr.Message)
case LevelError:
log.Error(appErr.Message, "error", appErr.Err)
http.Error(w, "服务器错误", 500)
case LevelFatal:
log.Fatal(appErr.Message, "error", appErr.Err)
}
}
}
}
七、最佳实践总结
7.1 DO(推荐做法)
// ✅ 始终检查错误
result, err := doSomething()
if err != nil {
return fmt.Errorf("操作失败:%w", err)
}
// ✅ 添加上下文信息
if err != nil {
return fmt.Errorf("处理用户 %s 失败:%w", userID, err)
}
// ✅ 使用 errors.Is 和 errors.As
if errors.Is(err, os.ErrNotExist) {
// 处理
}
// ✅ 定义哨兵错误
var ErrNotFound = errors.New("not found")
// ✅ 使用有意义的错误消息
return errors.New("用户名不能为空")
7.2 DON’T(避免做法)
// ❌ 忽略错误
result, _ := doSomething()
// ❌ 空错误检查
if err != nil {
// 什么都不做
}
// ❌ 模糊的错误消息
return errors.New("error")
// ❌ 直接比较错误(使用 errors.Is)
if err == os.ErrNotExist { // 不推荐
}
// ❌ 过度使用 panic
if err != nil {
panic(err) // 除非是真正不可恢复的错误
}
7.3 错误处理检查清单
- 所有错误都已处理或记录
- 错误消息包含足够上下文
- 使用
%w包装底层错误 - 使用
errors.Is检查特定错误 - 使用
errors.As提取错误类型 - 不在公共 API 中暴露内部错误细节
- 资源已正确清理(使用 defer)
- 日志包含错误堆栈(如需要)
八、常用错误处理库
8.1 标准库
import (
"errors" // 基础错误
"fmt" // 错误包装
)
8.2 第三方库
// github.com/pkg/errors - 堆栈追踪
import "github.com/pkg/errors"
err := errors.Wrap(originalErr, "context")
// github.com/go-playground/validator - 验证错误
import "github.com/go-playground/validator/v10"
validate := validator.New()
err := validate.Struct(data)
// go.uber.org/multierr - 错误合并
import "go.uber.org/multierr"
multierr.Append(&err, err1)
multierr.Append(&err, err2)
总结
Go 的错误处理哲学可以概括为:显式、简洁、可追踪。
| 特性 | 说明 |
|---|---|
| 显式处理 | 错误是值,需要显式检查 |
| 错误包装 | 使用 %w 添加上下文 |
| 类型安全 | 使用 errors.Is/As 检查类型 |
| 适度 panic | 仅用于不可恢复的错误 |
掌握这些最佳实践,能帮助你写出更健壮、更易维护的 Go 代码。