<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          關(guān)于 Golang 錯(cuò)誤處理的一些思考?

          共 6844字,需瀏覽 14分鐘

           ·

          2020-08-18 00:15

          寫在前面:如果你還沒在 error 上栽跟頭,那么當(dāng)你栽了跟頭時(shí)才會(huì)哭著想起來,當(dāng)年為什么沒好好思考和反省錯(cuò)誤處理這么一個(gè)宏大的話題

          關(guān)于 Golang 錯(cuò)誤處理的實(shí)踐

          Golang 有很多優(yōu)點(diǎn),這也是它如此流行的主要原因。但是 Go 1 對(duì)錯(cuò)誤處理的支持過于簡(jiǎn)單了,以至于日常開發(fā)中會(huì)有諸多不便利,遭到很多開發(fā)者的吐槽。這些不足催生了一些開源解決方案。與此同時(shí), Go 官方也在從語言和標(biāo)準(zhǔn)庫層面作出改進(jìn)。這篇文章將給出幾種常見創(chuàng)建錯(cuò)誤的方式并分析一些常見問題,對(duì)比各種解決方案,并展示了迄今為止(go 1.13)的最佳實(shí)踐。

          幾種創(chuàng)建錯(cuò)誤的方式

          首先介紹幾種常見的創(chuàng)建錯(cuò)誤的方法

          1. 基于字符串的錯(cuò)誤


          err1 := errors.New("math: square root of negative number")err2 := fmt.Errorf("math: square root of negative number %g", x)
          1. 帶有數(shù)據(jù)的自定義錯(cuò)誤

          package serr
          import ( "fmt" "github.com/satori/go.uuid" "log" "runtime/debug" "time")// 自定義基礎(chǔ)錯(cuò)誤類型type BaseError struct { InnerError error Message string StackTrace string Misc map[string]interface{}}
          func WrapError(err error, message string, messageArgs ...interface{}) BaseError { return BaseError{ InnerError: err, Message: fmt.Sprintf(message, messageArgs), StackTrace: string(debug.Stack()), Misc: make(map[string]interface{}), }}
          func (err *BaseError) Error() string {// 實(shí)現(xiàn) Error 接口 return err.Message}
          // 具體使用// "intermediate" moduletype IntermediateErr struct { error}
          func runJob(id string) error { const jobBinPath = "/bad/job/binary" isExecutable, err := isGloballyExec(jobBinPath) iferr != nil{ return IntermediateErr{wrapError( err, "cannot run job %q: requisite binaries not available", id, )} } else if isExecutable == false { return wrapError( nil, "cannot run job %q: requisite binaries are not executable", id, ) } return exec.Command(jobBinPath, "--id="+id).Run()}

          拋出問題

          開發(fā)中經(jīng)常需要檢查返回的錯(cuò)誤值并作相應(yīng)處理。下面給出一個(gè)最簡(jiǎn)單的示例。

          import (   "database/sql"   "fmt")
          func GetSql() error { return sql.ErrNoRows}
          func Call() error { return GetSql()}
          func main() { err := Call() if err != nil { fmt.Printf("got err, %+v\n", err) }}//Outputs:// got err, sql: no rows in result set

          有時(shí)需要根據(jù)返回的 error 類型作不同處理,例如:

          import (   "database/sql"   "fmt")
          func GetSql() error { return sql.ErrNoRows}
          func Call() error { return GetSql()}
          func main() { err := Call() 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

          實(shí)踐中經(jīng)常需要為錯(cuò)誤增加上下文信息后再返回,以方便調(diào)用者了解錯(cuò)誤場(chǎng)景。例如 Getcall 方法時(shí)常寫成:

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

          不過這個(gè)時(shí)候 err==sql.ErrNoRows 就不成立了。除此之外,上述寫法都在返回錯(cuò)誤時(shí)都丟掉了調(diào)用棧這個(gè)重要的信息。我們需要更靈活、更通用的方式來應(yīng)對(duì)此類問題。

          解決方案

          針對(duì)存在的不足,目前有幾種解決方案。這些方式可以對(duì)錯(cuò)誤進(jìn)行上下文包裝,并攜帶原始錯(cuò)誤信息, 還能盡量保留完整的調(diào)用棧

          方案 1:github.com/pkg/errors

          如果只有錯(cuò)誤的文本,我們很難定位到具體的出錯(cuò)地點(diǎn)。雖然通過在代碼中搜索錯(cuò)誤文本也是有可能找到出錯(cuò)地點(diǎn)的,但是信息有限。所以,在實(shí)踐中,我們往往會(huì)將出錯(cuò)時(shí)的調(diào)用棧信息也附加上去。調(diào)用棧對(duì)消費(fèi)方是沒有意義的,從隔離和自治的角度來看,消費(fèi)方唯一需要關(guān)心的就是錯(cuò)誤文本和錯(cuò)誤類型。調(diào)用棧對(duì)實(shí)現(xiàn)者自身才是是有價(jià)值的。所以,如果一個(gè)方法需要返回錯(cuò)誤,我們一般會(huì)使用 errors.WithStack(err) 或者 errors.Wrap(err,"custom message") 的方式,把此刻的調(diào)用棧加到error里去,并且在某個(gè)統(tǒng)一地方記錄日志,方便開發(fā)者快速定位問題。

          1. Wrap 方法用來包裝底層錯(cuò)誤,增加上下文文本信息并附加調(diào)用棧。一般用于包裝對(duì)第三方代碼(標(biāo)準(zhǔn)庫或第三方庫)的調(diào)用。

          2. WithMessage 方法僅增加上下文文本信息,不附加調(diào)用棧。如果確定錯(cuò)誤已被 Wrap 過或不關(guān)心調(diào)用棧,可以使用此方法。注意:不要反復(fù) Wrap ,會(huì)導(dǎo)致調(diào)用棧重復(fù)

          3. Cause 方法用來判斷底層錯(cuò)誤 。

          現(xiàn)在我們用這三個(gè)方法來重寫上面的代碼:

          import (   "database/sql"   "fmt"
          "github.com/pkg/errors")
          func GetSql() error { return errors.Wrap(sql.ErrNoRows, "GetSql failed")}
          func Call() error { return errors.WithMessage(GetSql(), "bar failed")}
          func main() { err := Call() 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, Call failed: GetSql failed: sql: no rows in result setsql: no rows in result setmain.GetSql /usr/three/main.go:11main.Call /usr/three/main.go:15main.main /usr/three/main.go:19runtime.main ...*/

          從輸出內(nèi)容可以看到, 使用 %v 作為格式化參數(shù),那么錯(cuò)誤信息會(huì)保持一行, 其中依次包含調(diào)用棧的上下文文本。使用 %+v ,則會(huì)輸出完整的調(diào)用棧詳情。如果不需要增加額外上下文信息,僅附加調(diào)用棧后返回,可以使用 WithStack 方法:

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

          注意:無論是 Wrap , WithMessage 還是 WithStack ,當(dāng)傳入的 err 參數(shù)為 nil 時(shí), 都會(huì)返回nil, 這意味著我們?cè)谡{(diào)用此方法之前無需作 nil 判斷,保持了代碼簡(jiǎn)潔

          方案 2:golang.org/x/xerrors

          結(jié)合社區(qū)反饋,Go 團(tuán)隊(duì)開始考慮在 Go 2 中簡(jiǎn)化錯(cuò)誤處理的提案。Go 核心團(tuán)隊(duì)成員 Russ Cox 在xerrors中部分實(shí)現(xiàn)了提案中的內(nèi)容。它用與 github.com/pkg/errors 相似的思路解決同一問題, 引入了一個(gè)新的 fmt 格式化動(dòng)詞: %w,使用 Is 進(jìn)行判斷。

          import (   "database/sql"   "fmt"
          "golang.org/x/xerrors")
          func Call() error { if err := GetSql(); err != nil { return xerrors.Errorf("bar failed: %w", GetSql()) } return nil}
          func GetSql() error { return xerrors.Errorf("GetSql failed: %w", sql.ErrNoRows)}
          func main() { err := Call() 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, Call failed: GetSql failed: sql: no rows in result setbar failed: main.Call /usr/four/main.go:12 - GetSql failed: main.GetSql /usr/four/main.go:18 - sql: no rows in result set*/

          與 github.com/pkg/errors 相比,它有幾點(diǎn)不足:

          1. 使用 : %w 代替了 Wrap , 看似簡(jiǎn)化, 但失去了編譯期檢查。如果沒有冒號(hào),或 : %w 不位于于格式化字符串的結(jié)尾,或冒號(hào)與百分號(hào)之間沒有空格,包裝將失效且不報(bào)錯(cuò);

          2. 而且,調(diào)用 xerrors.Errorf 之前需要對(duì)參數(shù)進(jìn)行nil判斷。這完全沒有簡(jiǎn)化開發(fā)者的工作

          方案 3:Go 1.13 內(nèi)置支持

          Go 1.13 將 xerrors 的部分功能(不是全部)整合進(jìn)了標(biāo)準(zhǔn)庫。它繼承了上面提到的 xerrors 的全部缺點(diǎn), 并額外貢獻(xiàn)了一項(xiàng)。因此目前沒有使用它的必要。

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

          上面的代碼與 xerrors 版本非常接近。但是它不支持調(diào)用棧信息輸出, 根據(jù)官方的說法, 此功能沒有明確的支持時(shí)間。因此其實(shí)用性遠(yuǎn)低于 github.com/pkg/errors

          Golang 中將來可能的錯(cuò)誤處理方式

          在 Go2 的草案中,我們看到了有關(guān)于 error 相關(guān)的一些提案,那就是 check/handle 函數(shù)。

          我們也許在下一個(gè)大版本的 Golang 可以像下面這樣處理錯(cuò)誤:

          import "fmt"func game() error {    handle err {        return fmt.Errorf("dependencies error: %v", err)    }
          resource := check findResource() // return resource, error defer func() { resource.Release() }()
          profile := check loadProfile() // return profile, error defer func() { profile.Close() }
          // ...}

          感興趣的同學(xué)可以關(guān)注下這個(gè)提案:https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md

          得出結(jié)論

          重要的是要記住,包裝錯(cuò)誤會(huì)使該錯(cuò)誤成為 API 的一部分。如果您不想將來將錯(cuò)誤作為 API 的一部分來支持,則不應(yīng)包裝該錯(cuò)誤。無論是否包裝錯(cuò)誤,錯(cuò)誤文本都將相同。那些試圖理解錯(cuò)誤的人將得到相同的信息,無論采用哪種方式; 是否要包裝錯(cuò)誤的選擇取決于是否要給程序提供更多信息,以便他們可以做出更明智的決策,還是保留該信息以保留抽象層。

          通過以上對(duì)比, 相信你已經(jīng)有了選擇。再明確一下我的看法,如果你正在使用 github.com/pkg/errors ,那就保持現(xiàn)狀吧。目前還沒有比它更好的選擇。如果你已經(jīng)大量使用 golang.org/x/xerrors , 別盲目換成 go 1.13 的內(nèi)置方案。

          總的來說,Go 在誕生之初就在各個(gè)方面表現(xiàn)得相當(dāng)成熟、穩(wěn)健。在演進(jìn)路線上很少出現(xiàn)猶疑和搖擺, 而在錯(cuò)誤處理方面卻是個(gè)例外。除了被廣泛吐槽的 if err != nil 之外, 就連其改進(jìn)路線也備受爭(zhēng)議、分歧明顯,以致于一個(gè)改進(jìn)提案都會(huì)因?yàn)閴旱剐缘姆磳?duì)意見而不得不作出調(diào)整。好在 Go 團(tuán)隊(duì)比以前更加樂于傾聽社區(qū)意見,團(tuán)隊(duì)甚至專門就此問題建了個(gè)反饋收集頁面。相信最終大家會(huì)找到更好的解決方案。




          推薦閱讀



          學(xué)習(xí)交流 Go 語言,掃碼回復(fù)「進(jìn)群」即可


          站長(zhǎng) polarisxu

          自己的原創(chuàng)文章

          不限于 Go 技術(shù)

          職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)


          Go語言中文網(wǎng)

          每天為你

          分享 Go 知識(shí)

          Go愛好者值得關(guān)注


          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  伊人综合成人网 | 色之综合天天综合色天天棕色 | 校园春色国产 | 一级电影网站 | 2019年情侣免费自拍视频青青 |