Go语言中优雅的处理错误,而不仅仅只是检查( 二 )


如果你的代码实现了一个约定指定错误类型的接口,那么所有这个接口的实现者,都要依赖于定义这个错误类型的包 。
对包的错误类型的过度暴露,使调用者和包之间产生了很强的耦合性,导致了 API 的脆弱性 。
结论:避免错误类型虽然错误类型在发生错误时能够捕捉到更多的环境信息,比标记错误要好一些,但是错误类型也存在很多和标记错误一样的问题 。
所以,在这里我的建议是避免使用错误类型,至少避免使他们成为你 API 接口的一部分 。
封装错误现在我们到了第三个错误处理分类 。在我看来这个是灵活性最好的处理策略,在调用者和你的代码之间产生的耦合度最低 。
我管这种处理方式叫做封装错误 (Opaque errors),因为当你发现有错误发生时,你无法知道内部的错误情况 。作为调用者,你只知道调用的结果成功或者失败 。
封装错误处理方式只返回错误不去猜测他的内容 。如果你采用了这种处理方式,那么错误处理在调试方面会变得非常有价值 。

Go语言中优雅的处理错误,而不仅仅只是检查

文章插图
 
例如:Foo 的调用约定没有指定在发生错误时会返回哪些相关信息,这样 Foo 函数的开发者就可以自由的提供相关错误信息,并且不会影响到和调用者之间的约定 。
断言行为,而不是类型在少数情况下,这种二元错误处理方案是不够的 。
例如:在和进程外交互的时候,比如网络活动,需要调用者评估错误情况来决定是否需要重试操作 。
在这种情况下我们断言错误实现指定的行为要比断言指定类型或值好些 。看看下面的例子:
Go语言中优雅的处理错误,而不仅仅只是检查

文章插图
 
我们可以传任何错误给 IsTemporary 来判断错误是否需要重试 。
如果错误没有实现 temporary 接口;那么就没有 Temporary 方法,那么错误就不是 temporary 。
如果错误实现了 Temporary,如果 Temporary 返回 true 那么调用者就可以考虑重试该操作 。
这里的关键点是这个实现逻辑不需要导入定义错误的包或者了解任何关于错误的底层类型,我们只要简单的关注它的行为即可 。
优雅的处理错误,而不仅仅只是检查错误这引出了我想谈的第二个 Go 语言的格言:优雅的处理错误,而不仅仅只是检查错误 。你能在下面的代码中找出错误么?
Go语言中优雅的处理错误,而不仅仅只是检查

文章插图
 
一个很明显的建议是上面的代码可以简化为
Go语言中优雅的处理错误,而不仅仅只是检查

文章插图
 
但是这只是个简单的问题,任何人在代码审查的时候都应该看到 。更根本的问题是这段代码看不出来原始错误是在哪里发生的 。
如果 authenticate 返回错误, 那么 AuthenticateRequest 将会返回错误给调用者,调用者也一样返回 。在程序的最上一层主函数块内打印错误信息到屏幕或者日志文件,然而所有信息就是 No such file or directory
Go语言中优雅的处理错误,而不仅仅只是检查

文章插图
 
没有错误发生的文件,行号等信息,也没有调用栈信息 。代码的编写者必须在一堆函数中查找哪个调用路径会返回 file not found 错误 。
Donovan 和 Kernighan 写的 The Go Programming Language 建议你使用 fmt.Errorf 在错误路径中增加相关信息
Go语言中优雅的处理错误,而不仅仅只是检查

文章插图
 
就像我们在前面提到的,这个模式不兼容标记错误或者类型断言,因为转换错误值到字符串,再和其他的字符串合并,再使用 fmt.Errorf 转换为error 打破了对等关系,破坏了原始错误的相关信息 。
注解错误我在这里建议一个给错误添加相关信息的方法,要用到一个简单的包 。代码在 github.com/pkg/errors 。这个包有两个主要的函数:
Go语言中优雅的处理错误,而不仅仅只是检查

文章插图
 
第一个函数是封装函数 Wrap ,输入一个错误和一个信息,生成一个新的错误返回 。
Go语言中优雅的处理错误,而不仅仅只是检查

文章插图
 
第二个函数是 Cause ,输入一个封装过的错误,解包之后得到原始的错误信息 。
使用这两个函数,我们现在可以给任何错误添加相关信息,并且在我们需要查看底层错误类型的时候可以解包查看 。下面的例子是读取文件内容到内存的函数 。


推荐阅读