学到了err(errors)

Mark wiens

发布时间:2023-12-23

跟DAVE大佬学习Go error处理姿势!

学到了err(errors)

 

这篇文章摘自DAVE在日本东京举行的GoCon春季会议上的演讲。

Errors仅仅就是返回值我花了很多时间思考在Go程序中处理错误的最佳方法我真的希望有一个单一的方法来处理错误,我们可以教所有Go程序员死记硬背,就像我们可能教数学,或字母表一样然而,我得出的结论是,处理错误没有单一的方法。

相反,我认为Go的错误处理可以分为三个核心策略,下文揭晓Sentinel errors错误处理的第一类是我所说的Sentinel errors(哨兵错误)if err == ErrSomething { … }。

这个名字来源于计算机编程中使用一个特定值来表示不可能进行进一步处理的做法所以对于Go,我们使用特定的值来表示错误示例包括类似值io.EOF或低级错误(如syscall包中的常量)syscall.ENOENT。

甚至存在前哨错误,表示未发生错误,例如go/build.NoGoError, and path/filepath.SkipDir from path/filepath.Walk.使用哨兵值是最不灵活的错误处理策略,因为调用方必须使用相等运算符将结果与预先声明的值进行比较。

当您想提供更多上下文时,这会带来一个问题,因为返回不同的错误将破坏相等性检查甚至fmt.Errorf在向错误中添加上下文等含义也将使调用方的相等性测试失败相反,调用方将在输出被迫查看error的Error。

方法,看它是否一个特定的字符串切勿检查error.Error的输出顺便说一句,我认为代码不应该检查错误或者错误方法Error接口上的 Error 方法是为人而不是代码而设计的该字符串的内容属于日志文件,或显示在屏幕上。

你不应该试图通过检查来改变程序的行为我知道有时候这是不可能的,正如有人在twitter上指出的,这个建议不适用于编写测试更重要的是,在我看来,您应该尽量避免比较一个错误的字符串Sentinel errors 成为公共 API 的一部分。

如果您的公共函数或方法返回特定值的错误,则该值必须是公共的,并且必须记录在案但同时会增加API的表面积如果您的API定义了一个返回特定错误的接口,则该接口的所有实现都将被限制为仅返回该错误,即使它们可以提供更具描述性的错误。

我们看到了io.Reader、io.Copy的实现,他们是通过返回io.EOF信号以不再向调用者发出信号,但这不是errorSentinel errors 在两个软件包之间创建了依赖关系到目前为止,sentinel errors

最糟糕的问题是它们在两个包之间创建了源代码依赖关系例如,检查错误是否等于io.EOF,您的代码必须导入io包这个特定的例子听起来并不那么糟糕,因为它非常常见,但是想象一下,当项目中的许多包导出错误值时,存在耦合,项目中的其他包必须导入这些错误值才能检查特定的错误条件。

我曾在一个大型项目中玩弄过这种模式,我可以告诉你,不良设计的幽灵——以导入循环的形式——从未远离我们的脑海结论:避免哨兵错误因此,我的建议是避免在编写的代码中使用Sentinel errors在某些情况下,标准库中会使用它们,但这不是您应该模仿的模式。

如果有人要求您从程序包中导出错误值,则应该礼貌地拒绝,而建议其他方法,例如我将在下面讨论的方法Error 类型错误类型是我要讨论的Go错误处理的第二种形式if err, ok := err.(SomeType); ok { … }。

错误类型是您创建的实现错误接口的类型在此示例中,MyError类型定义了File和Line,以及一条解释发生了什么的消息字段Msgtype MyError struct {        Msg string。

        File string        Line int}func (e *MyError) Error() string {return fmt.Sprintf("%s:%d: %s”, e.File, e.Line, e.Msg)

}return &MyError{"Something happened", “server.go", 42}因为MyError error是类型,所以调用者可以使用类型断言从错误中提取额外的上下文err := something()

switch err := err.(type) {case nil:        // call succeeded, nothing to docase *MyError:        fmt.Println(“error occurred on line:”, err.Line)

default:// unknown error}错误类型相对于错误值的一大改进是它们包装基本错误以提供更多上下文的能力一个很好的例子就是这种os.PathError类型,它在尝试执行的操作以及试图使用文件中携带了潜在错误。

// PathError records an error and the operation// and file path that caused it.type PathError struct {

        Op   string        Path string        Err  error // the cause}func (e *PathError) Error() string

Error 类型的问题调用者可以使用类型断言或类型开关,但是error类型必须公开如果您的代码实现了一个接口,该接口的约定需要特定的错误类型,那么该接口的所有实现者都需要依赖于定义错误类型的包这种对包类型的深入了解会导致与调用方的强耦合,从而导致。

API变得不可用结论:避免 Error 类型尽管Error类型比哨兵错误值更好,但是由于Error类型可以捕获有关错误原因的更多上下文,因此错误类型具有许多错误值问题所以我的建议还是避免error类型,或者至少避免使它们成为公共。

API的一部分Opaque errors现在我们来讨论第三类错误处理在我看来,这是最灵活的错误处理机制,因为它要求代码和调用者之间的耦合最少我将这种风格称为不透明错误处理,因为虽然您知道发生了错误,但您没有能力看到错误的内部。

作为调用者,关于操作的结果,您所知道的就是它起作用了,或者没有起作用这就是不透明错误处理的全部功能–只需返回错误而不假设其内容如果采用这种方式,那么错误处理作为调试辅助工具会变得更加有用import “github.com/quux/bar”。

func fn() error {        x, err := bar.Foo()if err != nil {return err        }        // use x}例如,Foo

函数不保证在出现错误的情况下会返回什么Foo的作者现在可以自由地用额外的上下文来注释通过它传递的错误,而不会破坏它与调用者的约定断言行为 error,而不是类型在少数情况下,这种二进制错误处理方法是不够的。

例如,与进程外的世界进行交互(如网络活动),需要调用方调查错误的性质,以确定重试该操作是否合理在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值考虑这个例子:type temporary interface {。

        Temporary() bool}// IsTemporary returns trueif err is temporary.func IsTemporary(err error) bool {

        te, ok := err.(temporary)return ok && te.Temporary()}我们可以将任何错误传递给istemorary,以确定是否可以重试该错误如果错误没有实现临时接口;也就是说,它没有临时方法,那么错误不是临时的。

如果错误确实实现了Temporary,那么如果Temporary返回true,那么调用者也许可以重试该操作这里的关键是,这个逻辑可以在不导入定义错误的包或者实际上不了解 error 的底层类型的情况下实现——我们只对它的行为感兴趣。

不要只是检查错误,请妥善处理这让我想到第二条格言,我想谈谈;不要只是检查错误,优雅地处理它们你能对下面的代码提出一些问题吗?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.。

没有生成错误的文件和行的信息没有导致错误的调用堆栈的堆栈跟踪这段代码的作者将被迫进行长时间的代码分割,以发现是哪个代码路径触发了文件未找到错误go编程语言建议您使用fmt.Errorffunc AuthenticateRequest(r *Request) error {。

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

return nil}但是正如我们前面看到的,这种模式与sentinel错误值或类型断言的使用不兼容,因为将错误值转换为字符串,将其与另一个字符串合并,然后将其转换回具有正式错误破坏相等并破坏原始错误中的任何上下文。

为 error 添加上下文我想建议一种方法来为错误添加上下文,为此,我将介绍一个简单的包代码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我们在 K&D 样式中得到了一个包裹良好的错误could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory。

因为errors.Wrap包裹了堆栈错误信息,理论上我们可以拿到详细堆栈错误信息,方法很简单,我们只需要把fmt.Println替换为errors.Printfunc main() {        _, err := ReadConfig()。

if err != nil {                errors.Print(err)                os.Exit(1)        }}我们就可以得到如下的错误信息:readfile.go:27: could not 

read configreadfile.go:14: open failedopen /Users/dfc/.settings.xml: no such file or directory第一行来自ReadConfig

,第二行来自操作系统打开ReadFile的一部分,其余部分来自os包本身,它不包含错误位置信息现在我们引入了wrap错误以生成堆栈的概念,现在我们需要讨论相反的情况,即unwrapping它们这是errors.Cause

功能// IsTemporary returns trueif err is temporary.func IsTemporary(err error) bool {        te, ok := errors.Cause(err).(temporary)。

return ok && te.Temporary()}当您需要使用特定的错误类型进行恢复时,应该首先使用errors.Cause来获取特定的错误类型仅仅处理一次错误最后,我想说的是,你应该只处理一次错误。

处理错误意味着检查错误值并做出决定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 callerreturn err        }return nil}在本例中,如果在写入过程中发生错误,则会将一行写入日志文件,记录发生错误的文件和行,并且错误也会返回给调用者,调用者可能会记录并返回,一直返回到程序的最上层。

因此,在日志文件中有一堆重复的行,但是在程序的顶部,您会得到没有任何上下文的原始错误func Write(w io.Write, buf []byte) error {        _, err := w.Write(buf)

return errors.Wrap(err, "write failed")}通过使用errors包,您可以向错误值添加上下文,这种方式既可以由人也可以由机器检查总结总之,error包是公共API的一部分,要像对待公共

API的任何其他部分一样对待它们为了获得最大的灵活性,我建议您尽量将所有错误视为不透明的在您不能这样做的情况下,断言行为错误,而不是操作类型或返回值最小化程序中的sentinel错误值的数量,一旦出现错误就用。

errors.Wrap将错误包装为不透明的错误最后,如果需要检查底层错误,则使用errors.Cause恢复错误推荐并非每个容器内部都能包含一个操作系统深入探究 K8S ConfigMap 和 Secret。

原创不易,随手关注或者”在看“,诚挚感谢!

免责声明:本站所有信息均搜集自互联网,并不代表本站观点,本站不对其真实合法性负责。如有信息侵犯了您的权益,请告知,本站将立刻处理。联系QQ:1640731186