任何返回错误的函数都应该努力使错误值变得有用。通常情况下,该函数处于一个调用链的中间,并且只是在传播它所调用的其他函数的错误(甚至可能来自另一个包)。这里有机会用额外的信息来注解错误,但程序员应该确保错误中有足够的信息,而不添加重复的或不相关的细节。如果你不确定,可以尝试在开发过程中触发错误条件:这是一个很好的方法来评估错误的观察者(无论是人类还是代码)最终会得到什么。
习惯和良好的文档有帮助。例如,标准包os
宣传其错误包含路径信息,当它可用时。这是一种有用的风格,因为得到错误的调用者不需要用他们已经提供了失败的函数的信息来注释它。
// Good:if err := os.Open("settings.txt"); err != nil { return err}// Output://// open settings.txt: no such file or directory
如果对错误的意义有什么有趣的说法,当然可以加入。只需考虑调用链的哪一层最适合理解这个含义。
// Good:if err := os.Open("settings.txt"); err != nil { // We convey the significance of this error to us. Note that the current // function might perform more than one file operation that can fail, so // these annotations can also serve to disambiguate to the caller what went // wrong. return fmt.Errorf("launch codes unavailable: %v", err)}// Output://// launch codes unavailable: open settings.txt: no such file or directory
与这里的冗余信息形成鲜明对比:
// Bad:if err := os.Open("settings.txt"); err != nil { return fmt.Errorf("could not open settings.txt: %w", err)}// Output://// could not open settings.txt: open settings.txt: no such file or directory
当添加信息到一个传播的错误时,你可以包裹错误或提出一个新的错误。用fmt.Errorf
中的%w
动词来包装错误,允许调用者访问原始错误的数据。这在某些时候是非常有用的,但在其他情况下,这些细节对调用者来说是误导或不感兴趣的。更多信息请参见关于错误包装的博文。包裹错误也以一种不明显的方式扩展了你的包的 API 表面,如果你改变了你的包的实现细节,这可能会导致破坏。
最好避免使用%w
,除非你也记录(并有测试来验证)你所暴露的基本错误。如果你不期望你的调用者调用errors.Unwrap
, errors.Is
等等,就不要费心使用%w
。
同样的概念适用于结构化错误,如*status.Status
(见规范错误码)。例如,如果你的服务器向后端发送畸形的请求,并收到一个InvalidArgument
错误码,这个代码不应该传播给客户端,假设客户端没有做错。相反,应该向客户端返回一个内部
的规范码。
然而,注解错误有助于自动日志系统保留错误的状态有效载荷。例如,在一个内部函数中注释错误是合适的:
// Good:func (s *Server) internalFunction(ctx context.Context) error { // ... if err != nil { return fmt.Errorf("couldn't find remote file: %w", err) }}
直接位于系统边界的代码(通常是RPC、IPC、存储等之类的)应该使用规范的错误空间报告错误。这里的代码有责任处理特定领域的错误,并以规范的方式表示它们。比如说:
// Bad:func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { // ... if err != nil { return nil, fmt.Errorf("couldn't find remote file: %w", err) }}
复制代码// Good:import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status")func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { // ... if err != nil { // Or use fmt.Errorf with the %w verb if deliberately wrapping an // error which the caller is meant to unwrap. return nil, status.Errorf(codes.Internal, "couldn't find fortune database", status.ErrInternal) }}