Golang惯用的错误处理方式

11
我已经创建了一个“基本”存储库结构,可用于独立和嵌入式使用(例如与CustomerRepository一起使用),以避免不断检查错误,并为Gorp(数据库工具包)创建抽象层,并创建一个更符合我的API。在这个基本结构中,我会检查错误并出现问题时会引发panic,因为在我看来,如果存在一个错误,则表示开发人员错误,代码可能会panic,因为验证等应该在数据到达存储库之前进行。我找到了这个问题Go Error Handling Techniques,但它没有涵盖像我所做的那样将错误封装在基本结构中并仅仅引发panic。我所做的是否符合Go语言惯例?
package repositories

import (
    "github.com/coopernurse/gorp"
)

type Repository struct {
    Gorp gorp.SqlExecutor
}

func (r *Repository) GetById(i interface{}, id int) interface{} {
    obj, err := r.Gorp.Get(i, id)
    if err != nil {
        panic(err)
    }
    return obj
}

func (r *Repository) Get(holder interface{}, query string, args ...interface{}) interface{} {
    if err := Gorp.SelectOne(holder, query, args); err != nil {
        panic(err)
    }
}

func (r *Repository) Select(i interface{}, query string, args ...interface{}) {
    if _, err := Gorp.Select(holder, query, args); err != nil {
        panic(err)
    }
}

func (r *Repository) Insert(list ...interface{}) {
    if err := r.Gorp.Insert(list...); err != nil {
        panic(err)
    }
}

func (r *Repository) Update(list ...interface{}) int64 {
    count, err := r.Gorp.Update(list...)
    if err != nil {
        panic(err)
    }
    return count
}

func (r *Repository) Delete(list ...interface{}) int64 {
    count, err := r.Gorp.Delete(list...)
    if err != nil {
        panic(err)
    }
    return count
}
3个回答

13

不要惊慌,这不是Go的方式。相反,应该像这样做 -

func (r *Repository) GetById(i interface{}, id int) (interface{}, error) {
    obj, err := r.Gorp.Get(i, id)
    if err != nil {
        return nil, err
    }
    return obj, nil
}

那么只需要在调用者中处理错误即可。从您上面的评论中可以看出,您正在使用这些函数作为Martini处理程序中的一部分,因此您可以进行如下操作--

func MyHandler(parameters) (int, string) {
    obj, err := repository.GetById(something, id)
    if err == repository.ErrNotFound {
        return http.StatusNotFound, fmt.Sprintf("could not find by id: %d", id)
    }
    if err != nil {
        return http.StatusInternalError, err.Error()
    }
    return http.StatusOk, fmt.Printf("obj: %v", obj)
}

这更像Go的方式。确保r.Gorp.Get返回您在包内声明的特定错误。

var ErrNotFound = errors.New("not found")

根据您的代码逻辑创建尽可能多的对象。


1
这正是我一直在寻找的。教程中 panic() 的普遍存在并不正确。 - Sean Lindo

10

最常用的方式是将错误与相关类型值一起返回,即

func (list ...interface{}) (v int46, err error) {}

在调用这些函数的地方检查err != nil。

最终使用panic()会导致类似于异常的错误处理和更多的样板代码(如果您认为错误是可恢复的)。

在Go中惯用的错误处理方式比模拟异常冗长,但不像模拟异常那样“非Go方式”。


那么错误应该逐个案例进行评估?我不确定这些错误是否可恢复,如果 GetById 失败,则实体不存在,如果删除失败,则由于编码错误(例如未正确处理外键等),情况非常糟糕。 - Lee
我原本并不打算从这些错误中恢复,我打算制造NotFoundError、NotAuthorizedError等错误,并将它们“panic”,然后在我的Web应用程序中使用Martini中间件拦截这些错误并显示404页面、403页面、500页面等。 - Lee
我认为是的,作为最佳实践 - 所有错误并不相等,就像你自己的例子中,我会为 GetById() 定义 ErrEntityNotFound = errors.New("entity not found")。如果你正在构建一个 API,Delete() 可能由于删除相同实体的顺序请求而失败 - 这值得 "panic" 吗?在我看来,你应该从返回的错误中确定错误级别。把所有玩具都扔出去似乎不是一个合理的处理方式。 - Martin Gallagher

0

虽然你在GitHub上看到的大部分代码,甚至是Go自己的库都使用错误作为返回值,但这种方法往往会使你的代码变得臃肿和复杂。 在使用Go几年后,我发现了一种更好的方法。 在你的代码中添加以下辅助函数:

func panicIfError(err error) {
 if err != nil {
  panic(err)
 }
}

func getNiceError(panicError any) error {
 stack := string(debug.Stack())
 index := strings.LastIndex(stack, "panic")
 if index != -1 {
  stack = stack[index:]
  index = strings.Index(stack, "\n")
  if index != -1 {
   stack = stack[index+1:]
  }
 }
 return fmt.Errorf("%v\n%v", panicError, stack)
}

func recoverError(recoveredError *error) {
 panicError := recover()
 if panicError == nil {
  recoveredError = nil
 } else {
  *recoveredError = getNiceError(panicError)
 }
}

在你的代码中,只在中央位置返回错误,例如应用程序HTTP服务器主入口来处理请求。所有其他函数不应返回错误,而应该触发panic异常:
func f3() {
 _, err := os.ReadFile("myfile.txt")
 panicIfError(err)
}

在您希望捕获错误并处理它的中心位置,创建一个函数来返回错误信息:
func centralHandlingFunc() (recoveredError error) {
 defer recoverError(&recoveredError)
 callToUnsafeFunc()
 return recoveredError
}

因此,99%的代码使用简单的panicIfError来处理错误。 这样你的代码更简单、更易读。 不仅如此,在出现错误时,你还可以获得完整的错误堆栈跟踪,大大简化了错误调查。

我写了一篇关于这个主题的文章,你可以阅读更多关于已知的错误处理方法以及关于这种特定方法的详细信息:https://runkiss.blogspot.com/2022/09/go-error-handling.html


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接