1.错误类型

声明错误可选的方式很少,在选择合适的错误申明方式之前,应考虑以下几点。

(1)调用者是否需要匹配错误以便他们可以处理它? 如果是,我们必须通过声明顶级错误变量或自定义错误类型来支持 errors.Is 或 errors.As 函数。

(2)错误消息是否为静态字符串,还是需要上下文信息的动态字符串? 如果是静态字符串,我们可以使用 errors.New,但对于后者,我们必须使用 fmt.Errorf 或自定义错误类型。

(3)我们是否正在传递由下游函数返回的新错误? 如果是这样,请参阅错误包装部分。

错误匹配 错误消息 指导
No static errors.New
No dynamic fmt.Errorf
Yes static top-level var with errors.New
Yes dynamic custom error type

如使用errors.New表示带有静态字符串的错误。如果调用者需要匹配并处理此错误,则将此错误导出为变量以支持将其与errors.Is匹配。

// Bad
// package foo

func Open() error {
  return errors.New("could not open")
}

// package bar

if err := foo.Open(); err != nil {
  // Can't handle the error.
  panic("unknown error")
}

// Good
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
  return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrCouldNotOpen) {
    // handle the error
  } else {
    panic("unknown error")
  }
}

对于动态字符串的错误, 如果调用者不需要匹配它,则使用fmt.Errorf,如果调用者确实需要匹配它,则自定义 error。

无错误匹配。

// package foo

func Open(file string) error {
  return fmt.Errorf("file %q not found", file)
}

// package bar

if err := foo.Open("testfile.txt"); err != nil {
  // Can't handle the error.
  panic("unknown error")
}

错误匹配。

// package foo

type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

func Open(file string) error {
  return &NotFoundError{File: file}
}


// package bar

if err := foo.Open("testfile.txt"); err != nil {
  var notFound *NotFoundError
  if errors.As(err, &notFound) {
    // handle the error
  } else {
    panic("unknown error")
  }
}

请注意,如果您从包中导出错误变量或类型, 它们将成为包公共 API 的一部分。

2.错误包装

在调用失败时传播错误有三个主要方式:

(1)返回原始错误。

(2)使用 fmt.Errorf 和 %w 添加上下文生成新错误。

(3)使用 fmt.Errorf 和 %v 添加上下文生成新错误。

如果没有要添加的其他上下文,则按原样返回原始错误。 这将保留原始错误类型和消息。 这非常适合底层错误消息有足够的信息来追踪它来自哪里的错误。

否则,尽可能在错误消息中添加上下文 这样就不会出现诸如“连接被拒绝”之类的模糊错误, 您会收到更多有用的错误,例如“呼叫服务 foo:连接被拒绝”。

使用 fmt.Errorf 为你的错误添加上下文, 根据调用者是否应该能够匹配和提取根本原因,在 %w 或 %v 格式之间进行选择。

(1)如果调用者应该可以访问底层错误,请使用 %w。 对于大多数包装错误,这是一个很好的默认值,但请注意,调用者可能会开始依赖此行为。因此,对于包装错误是已知变量或类型的情况,请将其作为函数契约的一部分进行记录和测试。

(2)使用 %v 来混淆底层错误,调用者将无法匹配它,但如果需要,您可以在将来切换到 %w。

在为返回的错误添加上下文时,通过避免使用 "failed to" 之类的短语来保持上下文简洁,当错误通过堆栈向上传递时,它会一层一层被堆积起来:

// Bad
s, err := store.New()
if err != nil {
    return fmt.Errorf("failed to create new store: %w", err)
}

// Good
s, err := store.New()
if err != nil {
    return fmt.Errorf("new store: %w", err)
}

然而,一旦错误被发送到另一个系统,应该清楚消息是一个错误(如 err 标签或日志中的 "failed" 前缀)。

另外请参考:Don't just check errors, handle them gracefully

powered by Gitbook该文章修订时间: 2024-03-22 15:30:00

results matching ""

    No results matching ""