妥善的错误处理对于维护应用程序的可靠性和用户友好性至关重要。通过在错误发生时及时处理,开发人员可以防止意外崩溃,从而增强程序的健壮性。有效的错误处理还能提供清晰的错误信息,帮助开发人员快速识别和解决问题,从而辅助调试。

在 Go 语言中,错误是通过 error 类型进行管理的,这提供了以下几个优点:

  • 促进程序流程的可预测性。
  • 便于立即进行错误检查和处理。
  • 降低忽略错误的风险。
  • 通过错误信息增强调试功能。

Go 程序使用 error 值来表示错误。Error 是任何实现了简单内置错误接口的类型:

type error interface {
  Error() string
}

当函数中可能出错时,该函数应在其最后一个返回值中返回 error 。任何调用可能返回 error 函数的代码都应该通过检查错误是否为 nil 来处理错误。

错误接口

我们来看看 Atoi 函数是如何使用这种模式的。Atoi 的签名如下:

func Atoi(s string) (int, error)

这意味着 Atoi 接受一个字符串参数,并返回两个值:一个整数和一个 error 。如果字符串可以成功转换为整数,Atoi 返回该整数和一个 nil 错误。如果转换失败,则返回零和一个非 nil 错误。以下是安全使用 Atoi 方法:

// Atoi 将包含数字的字符串转换为整数
i, err := strconv.Atoi("42b")
// 如果有错误
if err != nil {
  fmt.Println("couldn't convert:", err)
  // couldn't convert: strconv.Atoi: parsing "42b": invalid syntax
  // 'parsing "42b": invalid syntax' 是通过 .Error() 方法返回的
  return
}
// 如果执行到这里则成功转换

错误为 nil 表示成功;错误不是 nil 表示失败。由于错误只是接口,因此你可以构建自己的自定义类型来实现错误接口。下面是实现错误接口的 userError 结构的示例:

type userError struct {
  name string
}

func (e userError) Error() string {
  return fmt.Sprintf("%v has a problem with their account", e.name)
}

然后它可以用作错误:

func sendSMS(msg, userName string) error {
  if !canSendToUser(userName) {
      return userError{name: userName}
  }
  ...
}

Go 处理错误的方式相当独特。大多数语言将错误视为特殊和不同的东西。例如,Python 引发异常类型,JavaScript 引发并捕获错误。在 Go 中, 错误只是我们像处理任何其他值一样处理的另一个值 - 无论我们想要什么!没有任何特殊的关键字来处理它们。

错误包

Go 标准库提供了一个名为“errors”的包,使得处理错误变得容易。请阅读 errors.New() 函数的 godoc 文档,但这里有一个简单的例子:

var err error = errors.New("something went wrong")

恐慌

正如我们所见,在 Go 中处理错误的正确方法是使用 error 接口。将错误逐级传递到调用栈,并像处理普通值一样处理它们:

func enrichUser(userID string) (User, error) {
  user, err := getUser(userID)
  if err != nil {
      // fmt.Errorf is GOATed: it wraps an error with additional context
      return User{}, fmt.Errorf("failed to get user: %w", err)
  }
  return user, nil
}

然而,Go 语言还有另一种处理错误的方法: panic 函数。当一个函数调用 panic 时,程序会崩溃并打印堆栈跟踪信息。一般而言, 不要使用恐慌!panic 函数会将控制权从当前函数中抛出,并沿着调用栈向上传递,直到遇到一个延迟调用 recover 的函数。如果没有函数调用 recover ,则 goroutine(通常是整个程序)会崩溃。

func enrichUser(userID string) User {
  user, err := getUser(userID)
  if err != nil {
      panic(err)
  }
  return user
}

func main() {
  defer func() {
      if r := recover(); r != nil {
          fmt.Println("recovered from panic:", r)
      }
  }()

  // this panics, but the defer/recover block catches it
  // a truly astonishingly bad way to handle errors
  enrichUser("123")
}

有时,一些 Go 新手开发者看到 panic/recover 时会想:“这就像 try/catch !我喜欢这个!” 不要像他们那样。对于所有“正常”错误处理,我使用错误值;如果遇到真正无法恢复的错误,我会使用 log.Fatal 打印一条消息并退出程序。