try/catch

在这篇文章中,我将展示 Go 中处理错误的基础知识,以及一些你可以在代码中遵循的简单策略,以确保你的程序健壮且易于调试。

错误类型

Go 中的错误类型是通过以下接口实现的:

type error interface {
    Error() string
}
errorError()
构建错误
errorsfmterrors
package main

import "errors"

func DoSomething() error {
    return errors.New("something didn't work")
}
fmtintstringerror
package main

import "fmt"

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("can't divide '%d' by zero", a)
    }
    return a / b, nil
}
fmt.Errorf%w

在上述例子中,还有一些重要的事情需要注意。

errornilerrorif err != niltry/catcherrorinterrorerror零值error
定义预期的错误

Go 中另一个重要的技术是定义预期错误,这样就可以在代码的其他部分明确地检查这些错误。当你需要在遇到某种错误时执行不同的代码分支时,这就非常有用。

定义哨兵错误
Divideerrors.Iserror
package main

import (
    "errors"
    "fmt"
)

var ErrDivideByZero = errors.New("divide by zero")

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivideByZero
    }
    return a / b, nil
}

func main() {
    a, b := 10, 0
    result, err := Divide(a, b)
    if err != nil {
        switch {
        case errors.Is(err, ErrDivideByZero):
            fmt.Println("divide by zero error")
        default:
            fmt.Printf("unexpected division error: %s\n", err)
        }
        return
    }

    fmt.Printf("%d / %d = %d\n", a, b, result)
}
定义自定义错误类型

大多数错误处理的用例都可以用上面的策略来覆盖,然而,有时你可能需要一些额外的功能。也许你想让一个error携带额外的数据字段,或者当错误信息被打印出来时,能用动态值来填充自己。

你可以在 Go 中通过实现自定义错误类型来做到这一点。

DivisionErrorErrorerrors.AserrorDivisionError
package main

import (
    "errors"
    "fmt"
)

type DivisionError struct {
    IntA int
    IntB int
    Msg  string
}

func (e *DivisionError) Error() string { 
    return e.Msg
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivisionError{
            Msg: fmt.Sprintf("cannot divide '%d' by zero", a),
            IntA: a, IntB: b,
        }
    }
    return a / b, nil
}

func main() {
    a, b := 10, 0
    result, err := Divide(a, b)
    if err != nil {
        var divErr *DivisionError
        switch {
        case errors.As(err, &divErr):
            fmt.Printf("%d / %d is not mathematically valid: %s\n",
              divErr.IntA, divErr.IntB, divErr.Error())
        default:
            fmt.Printf("unexpected division error: %s\n", err)
        }
        return
    }

    fmt.Printf("%d / %d = %d\n", a, b, result)
}

注意:必要时,你也可以自定义errors.Is和errors.As的行为。请看这个 Go.dev 博客的一个例子。

另一个说明:errors.Is是在 Go 1.13 中添加的,比检查err == ...更可取。下面有更多关于这个问题的内容。

包装错误

errorerror
errorerror
errorserrors.Wraperrors.Unwraperrorerrorerror

一段历史: 在 2019 年 Go 1.13 发布之前,标准库并不包含很多处理错误的 API–基本上只有errors.New和fmt.Errorf。因此,你可能会在别的包里遇到没有实现一些较新错误 API 的遗留 Go 程序。许多遗留程序也使用第三方错误库,如 pkg/errors。最后,正式提案在 2018 年被记录下来,其中提出了许多我们今天在 Go 1.13+ 中看到的功能。

旧的方式(Go 1.13 之前)

通过看一些旧的 API 有局限性的例子,对比一下就很容易看出 Go 1.13+ 中新的错误 API 多么有用。

让我们考虑一个管理用户数据库的简单程序。在这个程序中,我们将有几个函数参与到数据库错误的生命周期中。

example.com/fake/users/db

我们还假设这个假数据库已经包含了一些查找和更新用户记录的功能。而且,用户记录被定义为一个结构体,看起来像:

package db

type User struct {
  ID       string
  Username string
  Age      int
}

func FindUser(username string) (*User, error) { /* ... */ }
func SetUserAge(user *User, age int) error { /* ... */ }

下面是我们的示例程序:

package main

import (
    "errors"
    "fmt"

    "example.com/fake/users/db"
)

func FindUser(username string) (*db.User, error) {
    return db.Find(username)
}

func SetUserAge(u *db.User, age int) error {
    return db.SetAge(u, age)
}

func FindAndSetUserAge(username string, age int) error {
  var user *User
  var err error

  user, err = FindUser(username)
  if err != nil {
      return err
  }

  if err = SetUserAge(user, age); err != nil {
      return err
  }

  return nil
}

func main() {
    if err := FindAndSetUserAge("bob@example.com", 21); err != nil {
        fmt.Println("failed finding or updating user: %s", err)
        return
    }

    fmt.Println("successfully updated user's age")
}
malformed reques
main
failed finding or updating user: malformed request
FindUserSetUserAge

Go 1.13 增加了一个简单的方法来添加这些信息。

错误更好地被包装起来
fmt.Errorf%w
package main

import (
    "errors"
    "fmt"

    "example.com/fake/users/db"
)

func FindUser(username string) (*db.User, error) {
    u, err := db.Find(username)
    if err != nil {
        return nil, fmt.Errorf("FindUser: failed executing db query: %w", err)
    }
    return u, nil
}

func SetUserAge(u *db.User, age int) error {
    if err := db.SetAge(u, age); err != nil {
      return fmt.Errorf("SetUserAge: failed executing db update: %w", err)
    }
}

func FindAndSetUserAge(username string, age int) error {
  var user *User
  var err error

  user, err = FindUser(username)
  if err != nil {
      return fmt.Errorf("FindAndSetUserAge: %w", err)
  }

  if err = SetUserAge(user, age); err != nil {
      return fmt.Errorf("FindAndSetUserAge: %w", err)
  }

  return nil
}

func main() {
    if err := FindAndSetUserAge("bob@example.com", 21); err != nil {
        fmt.Println("failed finding or updating user: %s", err)
        return
    }

    fmt.Println("successfully updated user's age")
}

如果我们重新运行程序并遇到同样的错误,日志应该打印如下:

failed finding or updating user: FindAndSetUserAge: SetUserAge: failed executing db update: malformed request
db.SetUserAg

如果使用得当,错误包装可以提供关于错误脉络的额外内容,其方式类似于传统的堆栈跟踪。

errors.Iserrors.Aserrors.Unwrap

好奇地想知道错误包装是如何工作的?看看 fmt.Errorf, %w 动词 和 错误 API的内部细节吧。

何时包装

一般来说,在每次 “冒泡” 时,即每次从一个函数中收到错误并想继续将其返回到函数链中时,至少用函数的名称来包裹错误是个好主意。

Wrapping an error adds the gift of context 然而,也有一些例外情况,在这种情况下,包装错误可能是不合适的。

由于包装错误总是保留原始的错误信息,有时暴露这些潜在的问题可能是一个安全、隐私,甚至是用户体验的问题。在这种情况下,可能值得处理错误并返回一个新的错误,而不是包装它。如果你正在编写一个开源库或 REST API,不希望将底层错误信息返回给第三方用户,就可能是这种情况。

结论

Errorinterface

参考文献

  • Error handling and Go

  • Go 1.13 Errors

  • Go Error Doc

  • Go By Example: Errors

  • Go By Example: Panic

原文信息

本文永久链接:https://github.com/gocn/translator/blob/master/2022/w06_effective_error_handling_in_golang.md

译者:Cluas

想要了解关于 Go 的更多资讯,还可以通过扫描的方式,进群一起探讨哦~