译|Don’t just check errors, handle them gracefully(二)

简介: 译|Don’t just check errors, handle them gracefully(二)

Opaque errors

现在我们来看第三类错误处理。 在我看来,这是最灵活的错误处理策略,因为它需要的代码和调用者之间的耦合最小。

我将这种方式称为不透明的错误处理,因为虽然您知道发生了错误,但您无法查看错误内部。 作为调用者,您对操作结果的所有了解都是有效的,或者没有。

这就是不透明的错误处理 - 只返回错误而不假设其内容。 如果采用此方式,则错误处理可以作为调试辅助工具,变得非常有用。

import “github.com/quux/bar”
func fn() error {
  x, err := bar.Foo()
  if err != nil {
    return err
  }
  // use x
}

例如,Foo 的契约不保证它将在错误的上下文中返回什么。通过传递错误附带额外的上下文,Foo 的作者现在可以自由地注释错误,而不会违反与调用者的契约。

Assert errors for behaviour, not type

在少数情况下,使用二分法(是否有错误)来进行错误处理是不够的。

例如,与进程外部的服务(例如网络活动)的交互,要求调用者查看错误的性质,以确定重试操作是否合理。

在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。 考虑这个例子:

type temporary interface {
  Temporary() bool
}
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
  te, ok := err.(temporary)
  return ok && te.Temporary()
}

可以将任何错误传递给 IsTemporary 以确定错误是否可以重试。

如果错误没有实现 temporary 接口; 也就是说,它没有 Temporary 方法,那么错误不是临时的。

如果错误确实实现了 Temporary,那么如果 true 返回true ,调用者可以重试该操作。

这里的关键是,此逻辑可以在不导入定义错误的包,或者直接知道任何关于 err的基础类型的情况下实现 - 我们只是对它的行为感兴趣。

Don’t just check errors, handle them gracefully

让我想到了第二句Go谚语,我想谈谈; 不要仅仅检查错误,优雅地处理它们。 你能用以下代码提出一些问题吗?

func AuthenticateRequest(r *Request) error {
  err := authenticate(r.User)
  if err != nil {
    return err
  }
  return nil
}

一个明显的建议是,函数的五行可以替换为:

return authenticate(r.User)

但这是每个人都应该在代码审查中发现的简单问题。这段代码更根本的问题是无法分辨原始错误来自哪里。

如果 authenticate 返回错误,那么 AuthenticateRequest 会将错误返回给调用者,调用者也可能会这样做,依此类推。 在程序的顶部,程序的主体将错误打印到屏幕或日志文件,所有打印的都会是: No such file or directory

没有生成错误的文件和行的信息。 没有导致错误的调用堆栈的 stack trace。 该代码的作者将被迫进行一个长的会话,将他们的代码二等分,以发现哪个代码路径触发了文件未找到错误。

Donovan和Kernighan的_The Go Programming Language_建议您使用 fmt.Errorf 向错误路径添加上下文

func AuthenticateRequest(r *Request) error {
  err := authenticate(r.User)
  if err != nil {
    return **fmt.Errorf("authenticate failed: %v", err)**
  }
  return nil
}

但是正如我们之前看到的,这种模式与使用 sentinel error values 或类型断言不兼容,因为将错误值转换为字符串,将其与另一个字符串合并,然后使用 fmt.Errorf 将其转换回错误,破坏了相等性,同时完全破坏了原始错误中的上下文。

Annotating errors

我想建议一种方法来为错误添加上下文,为此,我将介绍一个简单的包。 该代码在 github.com/pkg/errors 提供。 错误包有两个主要函数:

// Wrap annotates cause with a message.
func Wrap(cause error, message string) error

第一个函数是 Wrap,它接收一个错误和一段消息,并产生一个新的错误。

// Cause unwraps an annotated error.
func Cause(err error) error

第二个函数是 Cause,它接收可能已被包装的错误,并将其解包以恢复原始错误。

使用这两个函数,我们现在可以注释任何错误,并在需要检查时恢复底层错误。 考虑一个将文件内容读入内存的函数的例子。

func ReadFile(path string) ([]byte, error) {
  f, err := os.Open(path)
  if err != nil {
    return nil, **errors.Wrap(err, "open failed")**
  }
  defer f.Close()
  buf, err := ioutil.ReadAll(f)
  if err != nil {
    return nil, **errors.Wrap(err, "read failed")**
  }
  return buf, nil
}

我们将使用此函数编写一个函数来读取配置文件,然后从 main 调用它。

func ReadConfig() ([]byte, error) {
  home := os.Getenv("HOME")
  config, err := ReadFile(filepath.Join(home, ".settings.xml"))
  return config, **errors.Wrap(err, "could not read config")**
}
func main() {
  _, err := ReadConfig()
  if err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

如果 ReadConfig 代码路径失败,因为我们使用了 errors.Wrap,我们在K&D样式中得到一个很好的注释错误。

could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory

因为 errors.Wrap 会产生堆栈错误,所以我们可以检查该堆栈以获取其他调试信息。 这又是一个相同的例子,但这次我们用 fmt.Println 替换 errors.Print

func main() {
  _, err := ReadConfig()
  if err != nil {
    errors.Print(err)
    os.Exit(1)
  }
}

我们会得到如下信息:

readfile.go:27: could not read config
readfile.go:14: open failed
open /Users/dfc/.settings.xml: no such file or directory

第一行来自 ReadConfig,第二行来自 ReadFileos.Open 部分,其余部分来自 os 包本身,它不携带位置信息。

现在我们已经介绍了包装错误生成堆栈的概念,我们需要讨论反向操作,展开它们。 这是 errors.Cause 函数的域。

// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
  te, ok := **errors.Cause(err)**.(temporary)
  return ok && te.Temporary()
}

在操作中,每当您需要检查错误是否与特定值或类型匹配时,您应首先使用 errors.Cause 函数恢复原始错误。

Only handle errors once

最后,我想提一下:你应该只处理一次错误。 处理错误意味着检查错误值并做出决定。

func Write(w io.Writer, buf []byte) {
  w.Write(buf)
}

如果不做决定,则忽略该错误。 正如我们在这里看到的那样,w.Write 的错误被丢弃了。

但是,针对单个错误做出多个决策也存在问题。

func Write(w io.Writer, buf []byte) error {
  _, err := w.Write(buf)
  if err != nil {
    // annotated error goes to log file
    log.Println("unable to write:", err)
    // unannotated error returned to caller
    return err
  }
  return nil
}

在此示例中,如果在 Write 期间发生错误,则会将一行写入日志文件,注意错误发生的文件和行,并且错误也会返回给调用者,调用者可能会将其记录并返回,一路回到程序的顶部。

因此,您在日志文件中获得了重复的行的堆栈,但是在程序的顶部,您将获得没有原始错误的任何上下文。 有人使用Java吗?

func Write(w io.Write, buf []byte) error {
  _, err := w.Write(buf)
  return **errors.Wrap(err, "write failed")**
}

使用 errors 包,您可以以人和机器都可检查的方式向错误值添加上下文。

Conclusion

总之,错误是包 public API 的一部分,对待它们就像对待 public API 的其他部分一样小心。

为了获得最大的灵活性,我建议您尝试将所有错误都视为不透明的。在不能这样做的情况下,断言行为错误,而不是类型或值错误。

最小化程序中的 sentinel error values,并在错误发生时立即用 errors.Wrap 将其包装,从而将错误转换为不透明错误。

最后,如果需要检查,请使用 errors.Cause 恢复底层错误。

原文:don’t just check errors, handle them gracefully

本文作者 : cyningsun

本文地址https://www.cyningsun.com/09-09-2019/dont-just-check-errors-handle-them-gracefully-cn.html

版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!

# Error handling

  1. 跨服务错误处理
  2. 译|Errors are values
目录
相关文章
|
7月前
Which two statements are true about Database Vault factors?
Which two statements are true about Database Vault factors?
63 1
|
5月前
|
Linux 开发工具
You could try using --skip-broken to work around the problem You could try running: rpm -Va --nofi
linux配置环境变量操作失误出现:/usr/libexec/grepconf.sh: line 5: grep: command not found 的解决办法
169 2
|
程序员 Go API
译|Don’t just check errors, handle them gracefully(一)
译|Don’t just check errors, handle them gracefully
82 0
|
Linux
ERROR: 2 matches found based on name: network product-server_default is ambiguous
ERROR: 2 matches found based on name: network product-server_default is ambiguous
165 0
|
关系型数据库 MySQL Linux
SQLSTATE[HY000]: General error: 1364 Field ‘xxx’ doesn't have a default value 解决办法
SQLSTATE[HY000]: General error: 1364 Field ‘xxx’ doesn't have a default value 解决办法
1684 0
error: ‘PRIO_PROCESS’ undeclared
error: ‘PRIO_PROCESS’ undeclared
98 0
|
安全 iOS开发 MacOS
“XXXXX” is damaged and can’t be opened. You should move it to the Trash 解决方案
“XXXXX” is damaged and can’t be opened. You should move it to the Trash 解决方案
603 0
|
Go iOS开发
The operation couldn’t be completed. Unable to log in with account 'myappleid'. An unexpected failure occurred while logging in (Underlying error code 1100).解决方法
The operation couldn’t be completed. Unable to log in with account 'myappleid'. An unexpected failure occurred while logging in (Underlying error code 1100).解决方法
462 0
|
Java Apache
Failed to place enough replicas
如果DataNode的dfs.datanode.data.dir全配置成SSD类型,则执行“hdfs dfs -put /etc/hosts hdfs:///tmp/”时会报如下错误: 2017-05-04 16:08:22,545 WARN org.
3313 0