if err != nil 写到怀疑人生?这篇整理 Go 错误处理的正确姿势——从基础到进阶,让你不再害怕 error。
Q1:Go 的 error 到底是什么?为什么不用 try-catch?
A:
1 2 3 4
| type error interface { Error() string }
|
Go 选择 显式错误返回 而非异常机制的原因:
|
try-catch (Java/Python) |
error return (Go) |
| 控制流 |
可以跳过任意层 |
逐层显式处理 |
| 忽略错误 |
容易忘记 catch |
编译器强迫你写 if err != nil |
| 性能 |
异常有栈开销 |
零开销(就是个接口值) |
| 可读性 |
正常逻辑和错误处理混在一起 |
分离清晰 |
代价就是代码里到处是 if err != nil。但这是特性不是 bug——它逼你面对每一个可能出错的地方。
Q2:三种创建 error 的方式分别什么时候用?
A:
方式一:errors.New / fmt.Errorf — 最简单
1 2 3 4 5
| err := errors.New("something wrong")
err := fmt.Errorf("user %d not found", userID)
|
适用场景:简单的、不需要特殊判断的错误。
方式二:自定义错误类型 — 需要程序化判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| type ValidationError struct { Field string Message string }
func (e *ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Message) }
func validate(user *User) error { if user.Age < 0 { return &ValidationError{Field: "age", Message: "年龄不能为负数"} } return nil }
|
方式三:Sentinel Error — 预定义的哨兵错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var ( ErrUserNotFound = errors.New("user not found") ErrUnauthorized = errors.New("unauthorized") )
func getUser(id int) (*User, error) { return nil, ErrUserNotFound }
if errors.Is(err, ErrUserNotFound) { }
|
选择指南:
| 场景 |
推荐方式 |
| 简单的一次性错误 |
errors.New / fmt.Errorf |
| 需要携带额外信息 |
自定义 error 类型 |
| 需要精确匹配做分支处理 |
Sentinel Error + errors.Is |
Q3:Error Wrapping(%w)怎么用?这是 Go 1.13 之后的标准
A: 在多层调用中保留完整错误链:
1 2 3 4 5 6 7 8 9 10 11 12
| func GetConfig() (*Config, error) { data, err := os.ReadFile("config.json") if err != nil { return nil, fmt.Errorf("read config file: %w", err) }
cfg, err := parseConfig(data) if err != nil { return nil, fmt.Errorf("parse config: %w", err) } return cfg, nil }
|
%v vs %w 的区别:
1 2
| fmt.Errorf("context: %v", err) fmt.Errorf("context: %w", err)
|
解包错误链:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| _, err := GetConfig() if err != nil { fmt.Println(err) fmt.Printf("%+v\n", err)
if errors.Is(err, os.ErrNotExist) { ... }
var pathErr *os.PathError if errors.As(err, &pathErr) { fmt.Println("出错的文件:", pathErr.Path) } }
|
规则:在库/包的边界处用 %w 包装,让调用者能获取完整的错误上下文。
Q4:panic 和 error 的边界在哪里?
A: 这是个重要的设计决策:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| func Divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil }
func MustGetEnv(key string) string { val := os.Getenv(key) if val == "" { panic(fmt.Sprintf("required env %s is missing", key)) } return val }
|
边界规则:
| 场景 |
用 error |
用 panic |
| 用户输入无效 |
✅ |
❌ |
| 网络请求失败 |
✅ |
❌ |
| 文件不存在 |
✅ |
❌ |
| 程序启动配置缺失 |
— |
✅ |
| 不可变的编程错误(如数组越界) |
— |
✅ |
| 数据库连接失败(初始化阶段) |
— |
✅ |
defer + recover 兜底:
1 2 3 4 5 6 7 8 9
| func SafeRun(fn func()) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic recovered: %v", r) } }() fn() return }
|
注意:不要用 panic/error 做正常控制流。Java 里 try-catch 当 if-else 用的坏习惯别带到 Go 来。
Q5:第三方错误库值得用吗?
A: 看情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import "github.com/pkg/errors"
err := errors.Wrap(originalErr, "context") errors.StackTrace(err)
import "github.com/hashicorp/go-multierror"
var result *multierror.Error result = multierror.Append(result, err1) result = multierror.Append(result, err2) return result.ErrorOrNil()
|
我的建议:
- Go 1.13+ 内置的 errors 包已经够用了,大部分场景不需要第三方库
- 如果确实需要带堆栈的错误信息,用
github.com/pkg/errors
- 合并多个错误场景用
hashicorp/go-multierror
- 别为了用而用,标准库优先
Q6:API 层的错误怎么处理才规范?
A: 一个实用的分层错误处理模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package handler
type APIError struct { Code int `json:"code"` Message string `json:"message"` Details string `json:"details,omitempty"` }
func (e *APIError) Error() string { return e.Message }
var ( ErrBadRequest = &APIError{Code: 400, Message: "Bad Request"} ErrUnauthorized = &APIError{Code: 401, Message: "Unauthorized"} ErrNotFound = &APIError{Code: 404, Message: "Not Found"} ErrInternalServer = &APIError{Code: 500, Message: "Internal Server Error"} )
func HandleError(w http.ResponseWriter, r *http.Request, err error) { var apiErr *APIError if errors.As(err, &apiErr) { w.WriteHeader(apiErr.Code) json.NewEncoder(w).Encode(apiErr) return } log.Printf("[ERROR] unexpected: %v\n", err) w.WriteHeader(500) json.NewEncoder(w).Encode(ErrInternalServer) }
|
这样业务逻辑只管返回 error,HTTP 层统一翻译成 API 响应。
下期预告
下一篇聊 Context 包——Go 并发控制的核心。超时控制、取消信号、跨 goroutine 传值,理解了 Context 才算真正会用 Go 写后端服务。
TODO: 检查一下你项目里的错误处理——有没有裸 panic() 散落在业务代码中?有没有应该用 errors.Is 却在用 == 直接比较字符串的地方?