avatarChe Dan

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

4498

Abstract

g">"foo failed"</span>) }

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">bar</span><span class="hljs-params">()</span></span> <span class="hljs-type">error</span> { <span class="hljs-keyword">return</span> errors.WithMessage(foo(), <span class="hljs-string">"bar failed"</span>) }

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { err := bar() <span class="hljs-keyword">if</span> errors.Cause(err) == sql.ErrNoRows { fmt.Printf(<span class="hljs-string">"data not found, %v\n"</span>, err) fmt.Printf(<span class="hljs-string">"%+v\n"</span>, err) <span class="hljs-keyword">return</span> } <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-comment">// unknown error</span> } }</pre></div><div id="a144"><pre><span class="hljs-section">/Output:</span></pre></div><div id="11a5"><pre>data not found, bar failed: foo failed: sql: no rows <span class="hljs-keyword">in</span> result set sql: no rows <span class="hljs-keyword">in</span> result set foo failed <span class="hljs-selector-tag">main</span><span class="hljs-selector-class">.foo</span> /usr/three/<span class="hljs-selector-tag">main</span><span class="hljs-selector-class">.go</span>:<span class="hljs-number">11</span> <span class="hljs-selector-tag">main</span><span class="hljs-selector-class">.bar</span> /usr/three/<span class="hljs-selector-tag">main</span><span class="hljs-selector-class">.go</span>:<span class="hljs-number">15</span> <span class="hljs-selector-tag">main</span><span class="hljs-selector-class">.main</span> /usr/three/<span class="hljs-selector-tag">main</span><span class="hljs-selector-class">.go</span>:<span class="hljs-number">19</span> runtime<span class="hljs-selector-class">.main</span> ...</pre></div><div id="f773"><pre><span class="hljs-comment">/</span></pre></div><p id="9379">从输出内容可以看到, 使用 <code>%v</code> 作为格式化参数,那么错误信息会保持一行, 其中依次包含调用栈的上下文文本。 使用 <code>%+v</code> ,则会输出完整的调用栈详情</p><p id="9b9a">如果不需要增加额外上下文信息,仅附加调用栈后返回,可以使用 <code>WithStack</code> 方法:</p><div id="1f5c"><pre><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span></span> <span class="hljs-type">error</span> { <span class="hljs-keyword">return</span> errors.WithStack(sql.ErrNoRows) }</pre></div><p id="063a">注意:无论是 <code>Wrap </code><code>WithMessage</code> 还是 <code>WithStack</code> ,当传入的 err 参数为 nil 时, 都会返回nil, 这意味着我们在调用此方法之前无需作 nil 判断,保持了代码简洁</p><h1 id="2e88">2. golang.org/x/xerrors</h1><p id="13af">结合社区反馈,Go 团队完成了在 Go 2 中<a href="https://go.googlesource.com/proposal/+/master/design/29934-error-values.md">简化错误处理的提案</a>。 Go核心团队成员 <a href="https://research.swtch.com/">Russ Cox</a> 在xerrors中部分实现了提案中的内容。它用与 <code>github.com/pkg/errors</code>相似的思路解决同一问题, 引入了一个新的 fmt 格式化动词<code>: %w</code>,使用 <code>Is</code> 进行判断。:</p><div id="8504"><pre><span class="hljs-keyword">import</span> ( <span class="hljs-string">"database/sql"</span> <span class="hljs-string">"fmt"</span>

<span class="hljs-string">"golang.org/x/xerrors"</span> )

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">bar</span><span class="hljs-params">()</span></span> <span class="hljs-type">error</span> { <span class="hljs-keyword">if</span> err := foo(); err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> xerrors.Errorf(<span class="hljs-string">"bar failed: %w"</span>, foo()) } <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span> }

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span></span> <span class="hljs-type">error</span> { <span class="hljs-keyword">return</span> xerrors.Errorf(<span class="hljs-string">"foo failed: %w"</span>, sql.ErrNoRows) }

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { err := bar() <span class="hljs-keyword">if</span> xerrors.Is(err, sql.ErrNoRows) { fmt.Printf(<span class="hljs-string">"data not found, %v\n"</span>, err) fmt.Printf(<span class="hljs-string">"%+v\n"</span>, err) <span class="hljs-keyword">return</span> }

Options

<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-comment">// unknown error</span> } }</pre></div><div id="e9a8"><pre><span class="hljs-attribute">/* Outputs</span><span class="hljs-punctuation">:</span></pre></div><div id="0471"><pre>data <span class="hljs-keyword">not</span> found, bar failed: foo failed: sql: <span class="hljs-keyword">no</span> rows in result <span class="hljs-keyword">set</span> bar <span class="hljs-comment">failed:</span> main.bar /usr/four/main.go:<span class="hljs-number">12</span>

  • foo failed: main.foo /usr/four/main.go:18
  • sql: no <span class="hljs-comment">rows in result set</span> <span class="hljs-comment">*/</span></pre></div><p id="85e3"><code>github.com/pkg/errors</code> 相比,它有几点不足:</p><ol><li>使用 <code>: %w</code> 代替了 Wrap , 看似简化, 但失去了编译期检查。 如果没有冒号,或 <code>: %w</code> 不位于于格式化字符串的结尾,或冒号与百分号之间没有空格,包装将失效且不报错</li><li>更严重的是, 调用 <code>xerrors.Errorf</code> 之前需要对参数进行nil判断。 这实际<b>完全没有简化</b>开发者的工作</li></ol><h1 id="308a">3. Go 1.13 内置支持</h1><p id="78dd">到了 Go 1.13 ,xerrors 的部分功能(不是全部,下面会说明)被整合进了标准库。 它继承了 xerrors的<b>全部缺点</b>, 并额外贡献了一项。<b>因此目前没有使用它的必要</b></p><div id="2fe3"><pre><span class="hljs-keyword">import</span> ( <span class="hljs-string">"database/sql"</span> <span class="hljs-string">"errors"</span> <span class="hljs-string">"fmt"</span> )

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">bar</span><span class="hljs-params">()</span></span> <span class="hljs-type">error</span> { <span class="hljs-keyword">if</span> err := foo(); err != <span class="hljs-literal">nil</span> { <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"bar failed: %w"</span>, foo()) } <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span> }

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">foo</span><span class="hljs-params">()</span></span> <span class="hljs-type">error</span> { <span class="hljs-keyword">return</span> fmt.Errorf(<span class="hljs-string">"foo failed: %w"</span>, sql.ErrNoRows) }

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> { err := bar() <span class="hljs-keyword">if</span> errors.Is(err, sql.ErrNoRows) { fmt.Printf(<span class="hljs-string">"data not found, %+v\n"</span>, err) <span class="hljs-keyword">return</span> } <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> { <span class="hljs-comment">// unknown error</span> } }</pre></div><div id="3ab1"><pre><span class="hljs-comment">/* Outputs: data not found, bar failed: foo failed: sql: no rows in result set */</span></pre></div><p id="2403">与 xerrors 版本非常接近。<b>但是它<a href="https://github.com/golang/go/issues/29934#issuecomment-489682919">不支持调用栈信息输出</a></b>根据<a href="https://github.com/golang/go/issues/34349">官方的说法</a>, 此功能没有明确时间表。因此其实用性远低于 <code>github.com/pkg/errors</code></p><h1 id="5f24">总结</h1><p id="215e">通过以上对比, 相信你已经有了选择。 再明确一下我的看法。 我的选择顺序是 <b>1 > 2 > 3</b></p><ul><li>如果你正在使用 <code>github.com/pkg/errors</code> ,保持现状。目前还没有比它更好的选择</li><li>如果你已经大量使用 <code>golang.org/x/xerrors</code> , 别盲目换成内置方案,它目前还不值得</li></ul><p id="91c4">总的来说,Go 在诞生之初就在各个方面表现得相当成熟、稳健。 在演进路线上很少出现犹疑和摇摆。 而在错误处理方面却是个例外。 除了被广泛吐槽的 <code>if err != nil </code>之外, 就连其改进路线也备受争议、分歧明显,以致于一个<a href="https://github.com/golang/go/issues/32437">改进提案</a>都会因为压倒性的反对意见而<a href="https://github.com/golang/go/issues/32437#issuecomment-512035919">不得不作出调整</a>。 好在Go 团队比以前更加乐于倾听社区意见,团队甚至专门就此问题建了个<a href="https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback">反馈收集页面</a>。相信最终大家会找到更好的解决方案</p><h1 id="0af3">写在最后</h1><p id="8b9f">虽然我们讨论了如何高效地包装错误,但与其它技术一样, 它只应该应用于适合的地方。 它不应该成为通行准则。</p><p id="f5a2">为什么呢? 因为当开始使用 <code>errors.Cause(err, sql.ErrNoRows)</code><code>xerrors.Is(err, sql.ErrNoRows)</code> 时, 就意味着 <code>sql.ErrNoRows</code> 作为实现细节被暴露给外界了, 它成了API的一部分。</p><p id="cd87">如果只是利用库代码进行业务开发, 包装后作判断的作法可以被理解和接受的。</p><p id="a90f">而对于API的定义者来说, 这个问题就变得格外重要。 也许更好的办法是定义一个基础错误类型, 然后从其中派生出具体错误实例, 并附带错误编码</p><p id="5536">全文完,欢迎留言与我讨论 ^-^</p><p id="f9a9">猜你喜欢:</p><ul><li><a href="https://readmedium.com/master-wire-cn-d57de86caa1b">一文读懂Wire</a></li><li><a href="https://readmedium.com/micro-in-action-getting-start-cn-99c870e078f">Micro in Action:入门</a></li></ul></article></body>

Golang 错误处理最佳实践

Golang有很多优点,这也是它如此流行的主要原因。但是Go 1 对错误处理的支持过于简单了,以至于日常开发中会有诸多不便利。

这些不足催生了一些开源解决方案。与此同时, Go 官方也在从语言和标准库层面作出改进。

本文将分析一些常见问题,对比各种解决方案,并展示了迄今为止(go 1.13)的最佳实践。

先说结论:建议使用 github.com/pkg/errors 进行错误处理。原因将在下面详细阐述

问题

Golang开发中经常需要检查返回的错误值并作相应处理。最简示例如下:

import (
   "database/sql"
   "fmt"
)

func foo() error {
   return sql.ErrNoRows
}

func bar() error {
   return foo()
}

func main() {
   err := bar()
   if err != nil {
      fmt.Printf("got err, %+v\n", err)
   }
}
//Outputs:
// got err, sql: no rows in result set

有时需要根据返回的error类型作不同处理,例如:

import (
   "database/sql"
   "fmt"
)

func foo() error {
   return sql.ErrNoRows
}

func bar() error {
   return foo()
}

func main() {
   err := bar()
   if err == sql.ErrNoRows {
      fmt.Printf("data not found, %+v\n", err)
      return
   }
   if err != nil {
      // Unknown error
   }
}
//Outputs:
// data not found, sql: no rows in result set

实践中经常需要为错误增加上下文信息后再返回,以方便调用者了解错误场景。例如 foo方法时常写成

func foo() error {
   return fmt.Errorf("foo err, %v", sql.ErrNoRows)
}

但这时 err == sql.ErrNoRows 便不再成立。除此之外,上述写法都在返回错误时都丢掉了调用栈这个重要的诊断信息。我们需要更灵活、更通用的方式来应对此类问题。

解决方案

针对Go的不足,目前有几种解决方案。他们可以作上下文包装,不丢失原始错误信息, 还能尽量保留完整的调用栈

1. github.com/pkg/errors

来自 Dave Cheney , 有三个关键方法

  1. Wrap 方法用来包装底层错误,增加上下文文本信息并附加调用栈。 一般用于包装对第三方代码(标准库或第三方库)的调用。
  2. WithMessage 方法仅增加上下文文本信息,不附加调用栈。 如果确定错误已被 Wrap 过或不关心调用栈,可以使用此方法。 注意:不要反复 Wrap ,会导致调用栈重复
  3. Cause方法用来判断底层错误 。

用这个包重写上述程序:

import (
   "database/sql"
   "fmt"

   "github.com/pkg/errors"
)

func foo() error {
   return errors.Wrap(sql.ErrNoRows, "foo failed")
}

func bar() error {
   return errors.WithMessage(foo(), "bar failed")
}

func main() {
   err := bar()
   if errors.Cause(err) == sql.ErrNoRows {
      fmt.Printf("data not found, %v\n", err)
      fmt.Printf("%+v\n", err)
      return
   }
   if err != nil {
      // unknown error
   }
}
/*Output:
data not found, bar failed: foo failed: sql: no rows in result set
sql: no rows in result set
foo failed
main.foo
    /usr/three/main.go:11
main.bar
    /usr/three/main.go:15
main.main
    /usr/three/main.go:19
runtime.main
    ...
*/

从输出内容可以看到, 使用 %v 作为格式化参数,那么错误信息会保持一行, 其中依次包含调用栈的上下文文本。 使用 %+v ,则会输出完整的调用栈详情

如果不需要增加额外上下文信息,仅附加调用栈后返回,可以使用 WithStack 方法:

func foo() error {
   return errors.WithStack(sql.ErrNoRows)
}

注意:无论是 Wrap WithMessage 还是 WithStack ,当传入的 err 参数为 nil 时, 都会返回nil, 这意味着我们在调用此方法之前无需作 nil 判断,保持了代码简洁

2. golang.org/x/xerrors

结合社区反馈,Go 团队完成了在 Go 2 中简化错误处理的提案。 Go核心团队成员 Russ Cox 在xerrors中部分实现了提案中的内容。它用与 github.com/pkg/errors相似的思路解决同一问题, 引入了一个新的 fmt 格式化动词: %w,使用 Is 进行判断。:

import (
   "database/sql"
   "fmt"

   "golang.org/x/xerrors"
)

func bar() error {
   if err := foo(); err != nil {
      return xerrors.Errorf("bar failed: %w", foo())
   }
   return nil
}

func foo() error {
   return xerrors.Errorf("foo failed: %w", sql.ErrNoRows)
}

func main() {
   err := bar()
   if xerrors.Is(err, sql.ErrNoRows) {
      fmt.Printf("data not found, %v\n", err)
      fmt.Printf("%+v\n", err)
      return
   }
   if err != nil {
      // unknown error
   }
}
/* Outputs:
data not found, bar failed: foo failed: sql: no rows in result set
bar failed:
    main.bar
        /usr/four/main.go:12
  - foo failed:
    main.foo
        /usr/four/main.go:18
  - sql: no rows in result set
*/

github.com/pkg/errors 相比,它有几点不足:

  1. 使用 : %w 代替了 Wrap , 看似简化, 但失去了编译期检查。 如果没有冒号,或 : %w 不位于于格式化字符串的结尾,或冒号与百分号之间没有空格,包装将失效且不报错
  2. 更严重的是, 调用 xerrors.Errorf 之前需要对参数进行nil判断。 这实际完全没有简化开发者的工作

3. Go 1.13 内置支持

到了 Go 1.13 ,xerrors 的部分功能(不是全部,下面会说明)被整合进了标准库。 它继承了 xerrors的全部缺点, 并额外贡献了一项。因此目前没有使用它的必要

import (
   "database/sql"
   "errors"
   "fmt"
)

func bar() error {
   if err := foo(); err != nil {
      return fmt.Errorf("bar failed: %w", foo())
   }
   return nil
}

func foo() error {
   return fmt.Errorf("foo failed: %w", sql.ErrNoRows)
}

func main() {
   err := bar()
   if errors.Is(err, sql.ErrNoRows) {
      fmt.Printf("data not found,  %+v\n", err)
      return
   }
   if err != nil {
      // unknown error
   }
}
/* Outputs:
data not found,  bar failed: foo failed: sql: no rows in result set
*/

与 xerrors 版本非常接近。但是它不支持调用栈信息输出根据官方的说法, 此功能没有明确时间表。因此其实用性远低于 github.com/pkg/errors

总结

通过以上对比, 相信你已经有了选择。 再明确一下我的看法。 我的选择顺序是 1 > 2 > 3

  • 如果你正在使用 github.com/pkg/errors ,保持现状。目前还没有比它更好的选择
  • 如果你已经大量使用 golang.org/x/xerrors , 别盲目换成内置方案,它目前还不值得

总的来说,Go 在诞生之初就在各个方面表现得相当成熟、稳健。 在演进路线上很少出现犹疑和摇摆。 而在错误处理方面却是个例外。 除了被广泛吐槽的 if err != nil 之外, 就连其改进路线也备受争议、分歧明显,以致于一个改进提案都会因为压倒性的反对意见而不得不作出调整。 好在Go 团队比以前更加乐于倾听社区意见,团队甚至专门就此问题建了个反馈收集页面。相信最终大家会找到更好的解决方案

写在最后

虽然我们讨论了如何高效地包装错误,但与其它技术一样, 它只应该应用于适合的地方。 它不应该成为通行准则。

为什么呢? 因为当开始使用 errors.Cause(err, sql.ErrNoRows)xerrors.Is(err, sql.ErrNoRows) 时, 就意味着 sql.ErrNoRows 作为实现细节被暴露给外界了, 它成了API的一部分。

如果只是利用库代码进行业务开发, 包装后作判断的作法可以被理解和接受的。

而对于API的定义者来说, 这个问题就变得格外重要。 也许更好的办法是定义一个基础错误类型, 然后从其中派生出具体错误实例, 并附带错误编码

全文完,欢迎留言与我讨论 ^-^

猜你喜欢:

Golang
Errorhanding
Programming
Recommended from ReadMedium