Go 语言的错误处理一直是社区讨论的热点。与 Java/C# 的 try-catch 不同,Go 采用显式的 error 返回值机制,这让错误处理更加透明,但也带来了代码冗长的问题。本文总结 Go 错误处理的最佳实践,帮你写出更健壮的 Go 代码。
一、Go 错误处理的基本原则
Go 的核心理念:错误是值,不是异常。函数通过返回 error 告知调用方发生了什么,调用方必须显式处理。
func ReadFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read file %s: %w", path, err)
}
return data, nil
}
关键点:用 fmt.Errorf 包裹错误时加上上下文信息,让调用链中的错误可追溯。
二、常见反模式
1. 滥用 panic
// 错误:用 panic 代替 error
func GetConfig(key string) string {
val := config[key]
if val == "" {
panic(fmt.Sprintf("config %s not found", key))
}
return val
}
// 正确:返回 error
func GetConfig(key string) (string, error) {
val, ok := config[key]
if !ok {
return "", fmt.Errorf("config %s not found", key)
}
return val, nil
}
规则:panic 只用于真正不可恢复的程序错误(如数组越界、nil 指针),业务逻辑的错误一律用 error。
2. 吞掉错误
// 错误:忽略 error
data, _ := os.ReadFile(path)
// 正确:至少记录日志
data, err := os.ReadFile(path)
if err != nil {
log.Printf("read file failed: %v", err)
return err
}
3. 返回 nil 值 + nil error
// 错误:返回 nil 但没有 error
func FindUser(id int) *User {
// 没找到时返回 nil,但调用方不知道是"没找到"还是"出错了"
}
// 正确:明确区分"没找到"和"出错"
func FindUser(id int) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, err // 出错
}
if user == nil {
return nil, ErrNotFound // 没找到
}
return user, nil
}
三、错误包装与拆包
1. %w 包装(Go 1.13+)
func ReadConfig() (Config, error) {
data, err := os.ReadFile("config.json")
if err != nil {
return Config{}, fmt.Errorf("read config: %w", err)
}
// ...
}
// 调用方可以用 errors.Is/As 拆包
if errors.Is(err, os.ErrNotExist) {
// 处理文件不存在
}
2. errors.Is vs errors.As
// 判断错误类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("路径:", pathErr.Path)
}
// 判断特定错误值
if errors.Is(err, io.EOF) {
// 文件结束
}
四、自定义错误类型
1. 简单哨兵错误
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrRateLimited = errors.New("rate limited")
)
2. 结构化错误
type AppError struct {
Code string
Message string
Err error
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// 使用
return &AppError{
Code: "DB_001",
Message: "query user failed",
Err: err,
}
五、减少重复的 if err != nil
1. 辅助函数封装
func must[T any](val T, err error) T {
if err != nil {
panic(err)
}
return val
}
// 仅在初始化阶段使用
config := must(LoadConfig("config.json"))
2. 错误处理中间件模式
func HandleError(fn func() error) {
if err := fn(); err != nil {
log.Printf("error: %v", err)
// 统一处理:上报、重试等
}
}
HandleError(func() error {
return DoSomething()
})
六、HTTP 服务中的错误处理
func GetUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
user, err := service.FindUser(id)
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
http.Error(w, "user not found", http.StatusNotFound)
case errors.Is(err, ErrUnauthorized):
http.Error(w, "unauthorized", http.StatusUnauthorized)
default:
log.Printf("internal error: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
json.NewEncoder(w).Encode(user)
}
七、goroutine 中的错误处理
func ProcessAll(items []Item) error {
errCh := make(chan error, len(items))
for _, item := range items {
go func(it Item) {
errCh <- process(it)
}(item)
}
var errs []error
for i := 0; i < len(items); i++ {
if err := <-errCh; err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return fmt.Errorf("%d errors occurred, first: %w", len(errs), errs[0])
}
return nil
}
八、最佳实践总结
- 显式处理错误:不要吞掉、不要用 _ 忽略
- 用 %w 包装错误:保留错误链,方便 errors.Is/As
- 添加上下文:fmt.Errorf 中加入操作名和参数
- 不用 panic:业务逻辑用 error,panic 只用于不可恢复的 bug
- 自定义错误类型:让调用方可以精确判断和处理
- 统一错误入口:HTTP 服务用中间件统一处理
关于我们
如果你在Go 开发、微服务架构、错误处理设计等方面需要帮助,尊云科技可以为你提供服务。
我们专注于网站建设、APP开发、小程序开发、PHP/Vue/UniApp开发、API接口、AI集成,拥有丰富的项目经验。
联系方式:
- 微信:yvsm316
- QQ:316430983
- 官网:https://zunyun.cc/
