虽然 Go 有一个简单的错误模型,但有时并不像人们想象的那么简单。
本文旨在解释什么是错误,以及评估如何处理错误处理的不同策略。
Go 中的错误是什么?
在详细介绍不同的错误处理策略之前,让我们快速浏览一下 go 中的错误意味着什么。
该类型是内置的 Go 接口类型,定义如下:error
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
换句话说,实现返回字符串的方法的所有内容都被视为类型。Error()error
值得一提的是,很多时候我们关心的只是检查错误是否为零。但是,在其他情况下,了解特定错误是有意义的,也许可以处理不同的错误场景。对于这些情况,重要的是我们如何创建这些错误以便轻松处理。
现在事不宜迟,让我们来看看在 Go 中处理错误的不同策略。
哨兵错误
哨兵错误是一个命名的错误值。通常,这些错误是导出的标识符,因为其想法是使用您的API的用户可以将错误与此给定值进行比较。一个例子是:
var CustomError = errors.New("this is fine")
现在我们已经定义了哨兵错误,我们将分析处理这些类型错误的两种方法。
检查错误。错误输出
假设我们正在使用一种特定方法,该方法在某些情况下可能会返回错误,我们希望以不同于任何其他错误类型的方式处理这些错误。我们可以想到的一种方法是使用 .CustomErrorerror.Error()
作为一个例子,假设我们已经实现了一个利用外部包中的函数的函数:makeRquestRequestimaginary
func makeRequest(...) error {
// ...
// This method may return the CustomError.
err := imaginary.Request("GET", url, payload)
// We just want to specifically handle the CustomError error. We'll
// ignore the rest of errors in this example.
if err.Error() == imaginary.CustomError.Error() {
// ...
}
}
正如您可能注意到的那样,这有效并实现了我们的目的,因此我们可以认为我们很好。但是,这种类型的错误检查非常脆弱,因为如果软件包的维护者决定更新 的字符串值 ,程序将在像这样检查此错误的所有位置中断。imaginaryCustomError
因此,检查错误输出绝不应用于错误处理。只有使用它将其输出发送到日志文件或仅打印值以供用户读取才有意义。
检查错误值 Is
从 Go 1.13 开始,处理哨兵错误的更好选择是使用标准库中的函数。该函数现在如下所示:errors.Is
func makeRequest(...) error {
// ...
// This method may return the CustomError.
err := apipkg.Request("GET", url, payload)
// Check whether the err value is the same as sentinel CustomError.
if errors.Is(err, imaginary.CustomError) {
// ..
}
}
由于错误包装功能,这种错误处理甚至更有意义。
与检查输出相比,这种方法的主要优点是,在这里我们将得到的误差与值进行比较。它不仅更干净,而且我们的代码会自动变得更加健壮,因为对字符串值的任何更改都不会最终导致代码中的重大更改。error.Error()CustomErrorCustomError
我们已经看到了两种处理哨兵错误的方法。但是,它的用法有一些我们需要知道的限制。
主要缺点是哨兵错误是静态值,因此我们无法向它们添加动态或上下文信息。在某些情况下,除了静态错误本身之外,我们还希望提供一些动态内容。例如,假设我们要返回一个错误,以反映在我们的数据集中找不到特定食物及其名称。
var (
ErrFoodNotFound = errors.New("food not found")
foods = make(map[string]food)
)
func FindFood(name string) (food, error) {
f, ok := foods[name]
if !ok {
return food{}, fmt.Errorf("food %v, error: %v", name, ErrFoodNotFound)
}
return f, nil
}
您可能已经观察到,每次执行中的错误值都会因食物的名称而异。因此,无法将误差值与前面提到的任何策略进行比较。
自定义错误类型
自定义错误是我们通过手动实现错误接口创建的一种类型。一个例子是:
type CustomError struct{}
func (c CustomError) Error() string {
return "this is fine"
}
Go 1.13之前,用户被迫使用类型断言来检查错误类型,但是随着新的错误包改版,现在我们有检查错误类型的功能。errors.As
// Before go 1.13 we needed to apply type assertion to check the error type.
if _, ok := err.(CustomError); ok {
// ...
}
if errors.As(err, &CustomError{}) {
// ...
}
自定义错误类型相对于 sentinel 错误的主要好处是,我们可以使用任何可能有助于用户找出问题的动态信息。
即便如此,权衡是我们需要为要实现的每个错误类型定义一个自定义结构。根据我们定义的错误数量,这可能会迅速增加相当大的开销。
在 Go 1.13 引入包装错误功能之前,此策略被广泛使用,我们将在下一节中看到该功能。它将使我们能够执行与自定义错误相同的操作,但具有更大的灵活性和简单性。
换行错误
当 Go 1.13 发布时,维护者决定扩展该方法以支持一个新的动词,该动词基本上以类似于不再维护的软件包的方法在引擎盖下执行错误包装。fmt.Errorf %w Wrap github.com/pkg/errors
正如我们在哨兵错误中看到的那样,拥有固定字符串有时可能是一个问题。我们无法向其添加动态内容,因为它会根据动态值进行修改。
但是,我们可以通过使用包装错误来解决此问题。让我们通过包装错误来更新哨兵错误中使用的示例。
var (
ErrFoodNotFound = errors.New("food not found")
foods = make(map[string]food)
)
func FindFood(name string) (food, error) {
f, ok := foods[name]
if !ok {
return food{}, fmt.Errorf("food %v, error: %w", name, ErrFoodNotFound)
}
return f, nil
}
您需要注意发现微小的差异。只需在创建错误时添加参数即可。这个新错误将具有动态值,但将保留原始错误。因此,我们可以使用以下方法来处理包装错误:%w ErrFoodNotFound
err := FindFood("bananas")
// Handle the error if it's an ErrFoodNotFound one.
if errors.Is(err, ErrFoodNotFound) {
// ...
}
在这里,我们可以通过按顺序解开第一个元素来观察使用的全部潜力,以查找与第二个参数匹配的错误。因此,我们可以根据需要添加任意数量的包装级别,并且仍然能够将其与.errors.Is%werrors.Is
因此,这种策略不仅在构建方式上更加优雅,而且允许我们以无缝的方式对错误使用动态值,同时仍然能够确定它所代表的错误。
结论
总结一下关键要点,我们可以说在 Go 中处理错误时需要考虑不同的事项:
首先,如果我们不关心错误是什么,只需检查其无效性;故事结束。尽管如此,很多时候我们需要知道错误是什么才能处理不同的错误场景。当这很重要时,我们应该主要考虑两个选项:
当我们不需要动态值时,哨兵错误。我们可以使用 .errors.Is
当我们需要将哨兵错误与动态信息相结合时,包装错误。
网友评论