<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>

          Go error 打印災(zāi)難 — CockroachDB errors 庫(第3篇)

          共 15073字,需瀏覽 31分鐘

           ·

          2020-12-26 15:58

          點擊上方藍(lán)色“Go語言中文網(wǎng)”關(guān)注,每天一起學(xué) Go

          這篇文章是關(guān)于 “CockroachDB errors 庫”[1]的系列文章的第 3 篇,“CockroachDB errors 庫”實際上是 Go 的標(biāo)準(zhǔn) errors 包的通用、開放源碼的替代品。

          Go 1.13 的標(biāo)準(zhǔn)庫采用了 Dave Cheney 自 2015 年以來對錯誤處理的主要貢獻(xiàn):將 Go 錯誤對象構(gòu)造為鏈表的想法。 唉,這種方式給 Go 開發(fā)人員造成了巨大的障礙:使打印錯誤對象變得困難、幾乎不可能。

          這就是我所說的 “Go error 打印災(zāi)難",下面我們將準(zhǔn)確地看到它是什么。

          提醒:Go 錯誤為什么是鏈表,怎么做的

          Go 的錯誤 API,從 v1.13 開始,如下所示:

          //?error?is?a?pre-defined?type.
          type?error?interface?{
          ???//?Error?returns?an?error's?short?message?string.
          ???//?This?is?used?e.g.?when?formatting?an?error?with?%s/%v.
          ???Error()?string
          }

          //?wrapper?can?be?implemented?by?additional?error
          //?“l(fā)ayers”,?to?decorate?an?error.?This?interface
          //?is?not?pre-defined?in?the?language?but?should?be
          //?implemented?by?API-conformant?error?decorators.
          //
          //?This?is?the?interface?that?powers?the?error?identification
          //?facilities?errors.Is()?and?errors.As().
          type?wrapper?interface?{
          ???//?Unwrap?accesses?the?next?layer?in?the?error?object.
          ???//?This?used?to?be?called?“Cause”?in?Dave?Cheney's
          ???//?pkg/errors?library.
          ???Unwrap()?error
          }

          使用此 API,Go 生態(tài)系統(tǒng)中的代碼可以通過兩種方式構(gòu)造 error 對象:

          • 使用類似 fmt.Errorf() 或 errors.New() 構(gòu)造"葉”錯誤;
          • 使用修飾層 "包裝" 錯誤,例如使用 errors.Wrap() 為錯誤增加前綴信息,errors.WithStack() 附加堆棧跟蹤或使用 %w 動詞的 fmt.Errorf() ,這是 1.13 新增的:err = fmt.Errorf("some context: %w", err)
          • 包裝類型通過實現(xiàn) Unwrap() 方法來聲明其"剝離"的能力。這是由 Go 的標(biāo)準(zhǔn)庫檢查和使用的,特別是 errors.Is(),它可以通過查看所有中間層來識別錯誤是否為特定類型的錯誤。

          抽象的鏈表使得使用來自不相關(guān)的 Go 包的修飾類型將修飾附加到任何錯誤對象成為可能。通過將層之間的關(guān)系定義為”只是 error",沒有包依賴項,也不會有循環(huán)導(dǎo)入的問題。它還使得可以跨不同的項目分離裝飾器/包裝器的開發(fā),同時保持互操作性。

          有關(guān)這個主題的詳細(xì)信息,請閱讀本系列中的上一篇文章:Go 標(biāo)準(zhǔn)錯誤 API[2]。

          提醒:Go 的格式化設(shè)施

          Go 庫中最常用的、功能最全的打印設(shè)施是標(biāo)準(zhǔn) fmt 包。它包含格式化各種 Go 為字符串,或輸出到文件、buffer、終端。

          例如 fmt.Println(v) 將 v 的值打印到終端。

          fmt 中的大多數(shù)打印函數(shù)共享基礎(chǔ)邏輯,為更強(qiáng)大的 Printf() API 提供支持。Printf() 使用格式字符串和變量參數(shù)列表,并顯示根據(jù)格式的參數(shù)。這直接派生自 C 中類似的標(biāo)準(zhǔn) API[3]。

          Go 的 fmt 可以打印任意數(shù)據(jù)類型,但 C 的 stdio 不能。

          這是使用預(yù)定義邏輯的組合來處理 Go 自己的基本類型,能智能遞歸地打印結(jié)構(gòu)類型、指針和數(shù)組類型,以及自定義類型:fmt 嘗試在傳遞給 Print 樣式函數(shù)的值上使用四個接口。

          • fmt .Formatter 接口定義了 Format(…) 方法,可以通過實現(xiàn)該方法以完全覆蓋格式。
          • 如果 fmt.Formatter 不存在,然后 fmt 會識別預(yù)定義的 error 接口。在這種情況下,它調(diào)用 Error() 方法并打印該方法。
          • 如果 fmt.Formatter 或 error 都不可用,則根據(jù)使用的格式動詞,自定義類型可以實現(xiàn) fmt.Stringer(一個 String() 方法)或 fmt.GoStringer(一個 GoString() 方法)用于驅(qū)動更簡單、不太靈活的格式輸出。

          只有當(dāng)這些接口都未實現(xiàn)時,fmt 才回退到使用其預(yù)定義邏輯。

          有關(guān)本主題的詳細(xì)信息,請閱讀本系列中的上一篇文章:Go 的格式化 API。[4]

          提醒:error 的簡單打印

          fmt 檢測 error 參數(shù)并自動調(diào)用 Error() 方法。這工作得很好,即使對于包裝錯誤:Error() 在最外層的包裝器(鏈接列表的頭部)上調(diào)用。因此,該包裝器的 Error() 實現(xiàn)可以覆蓋其尾部圖層的錯誤。

          例如:

          • errors.New("world").Error() 返回 world。
          • errors.Wrap(errors.New("world"), "hello") 返回 hello: world。
          • 同上,fmt.Errorf("hello %w", errors.New("world")) 也構(gòu)造了一個包裝錯誤。

          這樣,當(dāng)將錯誤傳遞到 fmt 時,我們會自動獲得自然的"更長","更完整”的 Error() 結(jié)果。

          一切似乎都很好,而且自從 Go v1.0 以來一直很好,但是詳細(xì)的打印又如何呢?

          提醒:詳細(xì)的打印模式

          當(dāng)使用 %+v 格式參數(shù)時,fmt 內(nèi)部邏輯將采用"詳細(xì)"模式,以顯示參數(shù)列表中的相應(yīng)值。默認(rèn)情況下,詳細(xì)模式會觸發(fā)例如在結(jié)構(gòu)類型中顯示字段名稱。

          例如:

          s?:=?struct?{?a?int?}{123}
          fmt.Printf("%v\n",?s)??//?prints?{123}
          fmt.Printf("%+v\n",?s)?//?verbose?mode:?prints?{a:123}

          "詳細(xì)模式”的定義有一個新的抽象:某些數(shù)據(jù)在常見情況下不可見,但可根據(jù)請求變?yōu)榭梢姟?/p>

          這在進(jìn)行 “printf debugging” 或事件日志記錄時非常有用,因為查看調(diào)試或日志記錄輸出是給專業(yè)用戶查看的,可以查看比程序常規(guī)輸出中顯示更多的信息。

          自定義類型只能通過實現(xiàn) fmt.Formatter 接口來自定義詳細(xì)模式的輸出。formatter 接口。只有該接口的 Format() 方法能獲取有關(guān)是否請求詳細(xì)模式的信息。fmt 包的其他接口不夠強(qiáng)大。

          特別是,error 接口和隱式 wrapper 接口都沒有為錯誤類型提供一種自定義 fmt 中顯示方式的方法。

          詳細(xì)打印 Go 錯誤的可取之處

          除了調(diào)用 Error() 方法提供的簡單模式外,Go 生態(tài)系統(tǒng)還構(gòu)建了單獨的詳細(xì)模式來打印錯誤對象的需求。

          例如,Dave Cheney 的 pkg/errors 包和 CockroachDB 的 errors[5] 庫都會自動在錯誤對象中嵌入堆棧跟蹤。此堆棧跟蹤不會出現(xiàn)在 Error() 的輸出中,因此在簡單模式下打印錯誤對象時不包括此堆棧跟蹤。當(dāng)程序遇到錯誤,發(fā)現(xiàn)自己無法令人滿意地處理它時,程序員可以使用 %+v 進(jìn)入詳細(xì)模式以查看堆棧跟蹤。這有助于了解錯誤的來源和在程序中的位置。

          此外,程序可以選擇使用錯誤包裝器將不是錯誤消息的控制信息嵌入到程序中,例如,指示調(diào)用方函數(shù)中錯誤處理期間應(yīng)執(zhí)行操作的特殊數(shù)字代碼。調(diào)用方函數(shù)可以使用標(biāo)準(zhǔn) API errors.As() 從錯誤鏈接鏈表中提取此數(shù)據(jù)。

          如果程序員在排除的疑難 Bug 時想要可視化此信息,該信息不包括在 Error() 的輸出中,怎么辦?同樣,將此信息輸出為"詳細(xì)模式”的一部分,似乎這是一種自然的選擇。

          不幸的是,實現(xiàn)這個目標(biāo)是相當(dāng)困難的。

          基本缺陷 1:在包裝器中無法自定義

          我們試驗和設(shè)計自己的錯誤類型,其中一些隱藏的信息只在詳細(xì)模式下顯示。我們可以這樣做,如下所示:

          type?myError?struct?{
          ???msg?string?//?public?message
          ???code?int?//?hidden?code
          }

          //?Error?implements?the?error?interface.
          func?(e?*myError)?Error()?string?{?return?e.msg?}

          //?Format?implements?the?fmt.Formatter?interface.
          func?(e?*myError)?Format(s?fmt.State,?verb?rune)?{
          ???if?verb?==?'v'?&&?s.Flag('+')?{
          ??????//?Verbose?mode.
          ??????fmt.Fprintf(s,?"(code:?%d)?%s",?e.code,?e.msg)
          ???}?else?{
          ??????fmt.Fprint(s,?e.msg)
          ???}
          }

          說明:當(dāng)我們用 %v 打印 *myError 的實例時,我們得到 msg 的值;使用 %+v 時,我們得到相同的內(nèi)容,但有前綴 (code: NNN) ?和字段 code 的值。

          精明的讀者可能會注意到此代碼看起來不完整,因為它不處理 %q 等格式動詞。這在本節(jié)中不直接相關(guān),因此我們暫時忽略它。

          除了最后一點, 代碼似乎工作正常?

          唉!

          嘗試以下代碼:

          err?:=?&myError{"hello",?123}
          err?=?fmt.Errorf("wazaa:?%w",?err)
          fmt.Println(err)?????????//?simple?mode:?prints?just?"wazaa:?hello"
          fmt.Printf("%+v\n",?err)?//?verbose:?prints...?what?

          我們希望本示例中的代碼打印 zawaa: (code: 213) hello。不幸的是,它不是:由 fmt.Errorf 返回的錯誤類型,fmt.Formatter 接口不起作用。因此,使用 fmt.Errorf 時,myError 中的自定義信息丟失了!

          換句話說,在 “標(biāo)準(zhǔn) Go” 中,通過 Unwrap() 方法創(chuàng)建良好的包裝錯誤類型還不夠;因此,在"標(biāo)準(zhǔn) Go"中創(chuàng)建成形良好的錯誤包裝類型是不夠的。還必須實現(xiàn)適當(dāng)?shù)?Format() 方法,在包裝錯誤中,通過 fmt.Formatter ?處理任何可能的自定義格式。

          這樣有兩個主要問題:

          • 有一點很明確:必須實現(xiàn) Format() 方法,即使自定義包裝不需要自定義格式,以免 fmt.Formatter 接口對于所有參與者都毫無用處。
          • Go 庫中沒有文檔說明此問題。所以大家根本不了解也不知道。實際上,粗略的檢查顯示,Go 生態(tài)系統(tǒng)中的許多自定義錯誤包裝器類型均未實現(xiàn) Format(),因此會在其 “尾巴” 中破壞格式自定義。

          基本缺陷 2:轉(zhuǎn)發(fā)(forwarding) fmt.Formatter 的困難

          如果我們愿意支付抽象稅,并同意所有包裝錯誤類型也將實現(xiàn) fmt.Formatter,那又會這樣?怎么會這樣呢?

          作為支持示例,讓我們嘗試一個非常簡單的包裝,它沒有任何特殊功能:

          type?myWrapper?struct?{
          ???cause?error?//?tail?of?linked?list
          }

          //?Error?implements?the?error?interface.
          func?(e?*myWrapper)?Error()?string?{?return?e.cause.Error()?}

          //?Unwrap?implements?the?unwrap?interface.
          func?(e?*myWrapper)?Unwrap()?error?{?return?e.cause?}

          然后,我們可以開始實現(xiàn) fmt.Formatter。至少,它應(yīng)該區(qū)分冗長和非冗長模式。

          但是,如果我們不確定錯誤原因(error cause)是否實際實現(xiàn) fmt.Formatter?也許沒有。因此,為了減少的驚訝,我們需要做"與 fmt 相同的一些事"。實現(xiàn)此目的的最佳方法是調(diào)用 fmt 本身邏輯:

          //?Format?implements?the?fmt.Formatter?interface.
          func?(e?*myWrapper)?Format(s?fmt.State,?verb?rune)?{
          ???if?verb?==?'v'?&&?s.Flag('+')?{
          ??????//?Verbose?mode.?Make?fmt?ask?the?cause
          ??????//?to?print?itself?verbosely.
          ??????fmt.Fprintf(s,?"%+v",?e.cause)
          ???}?else?{
          ??????//?Simple?mode.?Make?fmt?ask?the?cause
          ??????//?to?print?itself?simply.
          ??????fmt.Fprint(s,?e.cause)
          ???}
          }

          這是一個繁瑣的模式,只是為了確保 e. cause 得到打印。

          此外,如果 e.cause 想要了解有關(guān)原始格式的信息,那該內(nèi)容會如何呢?如果與 %#v 一起使用時,使用 #v?還是 %#+v?還是 %q?

          遺憾的是,fmt 中沒有標(biāo)準(zhǔn) API 來正確將所有狀態(tài)轉(zhuǎn)發(fā)到遞歸調(diào)用。自 Go 1.15 起,將所有格式狀態(tài)(formatting state)完全轉(zhuǎn)發(fā)到錯誤原因而不打印任何其他內(nèi)容的代碼量最低如下:

          //?Format?implements?the?fmt.Formatter?interface.
          func?(e?*myWrapper)?Format(s?fmt.State,?verb?rune)?{
          ????var?f?strings.Builder
          ????f.WriteByte('%')
          ????if?s.Flag('+')?{
          ????????f.WriteByte('+')
          ????}
          ????if?s.Flag('-')?{
          ????????f.WriteByte('-')
          ????}
          ????if?s.Flag('#')?{
          ????????f.WriteByte('#')
          ????}
          ????if?s.Flag('?')?{
          ????????f.WriteByte('?')
          ????}
          ????if?s.Flag('0')?{
          ????????f.WriteByte('0')
          ????}
          ????if?w,?wp?:=?s.Width();?wp?{
          ????????f.WriteString(strconv.Itoa(w))
          ????}
          ????if?p,?pp?:=?s.Precision();?pp?{
          ????????f.WriteByte('.')
          ????????f.WriteString(strconv.Itoa(p))
          ????}
          ????f.WriteRune(verb)
          ????fmt.Fprintf(f.String(),?e.cause)
          }

          這看起來非常不方便,容易出錯。

          即使是 Dave Cheney 的 pkg/errors 包也沒有做到這一點,它僅在包裝器中按如下方式實現(xiàn) Format(),如下所示:

          func?(w?*withMessage)?Format(s?fmt.State,?verb?rune)?{
          ????switch?verb?{
          ????case?'v':
          ????????if?s.Flag('+')?{
          ????????????fmt.Fprintf(s,?"%+v\n",?w.Cause())
          ????????????io.WriteString(s,?w.msg)
          ????????????return
          ????????}
          ????????fallthrough
          ????case?'s',?'q':
          ????????io.WriteString(s,?w.Error())
          ????}
          }

          此代碼對于謂詞 %q 不正確,同時完全省略其他格式標(biāo)記(如 %#v 等),并且無法識別除 v、s 或 q 以外的任何謂詞。

          在 Go 生態(tài)系統(tǒng)中探索發(fā)現(xiàn),很少有自定義錯誤包裝類型實現(xiàn) Format()。

          實現(xiàn)適當(dāng)?shù)淖远x Format(), 以及沒有預(yù)定義 (也不建議) 機(jī)制在 fmt 中轉(zhuǎn)發(fā) Format() 調(diào)用這一事實是如此困難,這是 Go 標(biāo)準(zhǔn)庫的基本限制。

          (安利:上面的正確代碼的副本可作為可重用的 fmtfwd.MakeFormat() 函數(shù),在 go-mtfwd[6] 包中。然而,這不是萬能藥。)

          基本缺陷 3:不更改 API 無法修復(fù)的問題

          Go 的團(tuán)隊稱自己構(gòu)建的語言可以最大限度地保持向后兼容性。標(biāo)準(zhǔn)庫的添加是通過引入或替換功能,但不會影響現(xiàn)有代碼的語義。

          在這種情況下,Go 開發(fā)人員可以做什么來"修復(fù)"上面確定的問題,而不破壞現(xiàn)有的 error 代碼,也不需要現(xiàn)有包添加"缺失"的粘附代碼,如缺少的 Format() 轉(zhuǎn)發(fā)器?

          事實證明,在 fmt 包中可以直接做的工作不多。

          在高級別上,不可能的任務(wù)是確保錯誤鏈中的所有細(xì)節(jié)以詳細(xì)模式打印,同時考慮 Format() 方法中的自定義行為。

          由于不是鏈中的每一個錯誤都提供 Format() 方法,因此 fmt 代碼需要使用 Unwrap() 方法迭代自身。然后在每個層上都需要打印...東西。但究竟是什么?

          • 它無法調(diào)用 Error(),因為包裝器上的 Error() 本身將遞歸,并獲取鏈中其他層的字符串片段;
          • 它無法調(diào)用 Format(),因為包裝器上的 Format() 已經(jīng)(根據(jù)當(dāng)前生態(tài)系統(tǒng))對錯誤原因遞歸遞處理。

          因為 fmt.Formatter 接口 Format() 方法的第一個參數(shù) fmt.State,是一個接口類型,因此實際在 fmt 中會是一個特定的 State 實例,可以"分離"當(dāng)前錯誤層內(nèi)的直接打印,從進(jìn)一步遞歸執(zhí)行打印。

          例如,如下 Format() 實現(xiàn):

          //?Format?implements?the?fmt.Formatter?interface.
          func?(e?*myWrapper)?Format(s?fmt.State,?verb?rune)?{
          ???if?verb?==?'v'?&&?s.Flag('+')?{
          ??????//?Verbose?mode.?Make?fmt?ask?the?cause
          ??????//?to?print?itself?verbosely.
          ??????fmt.Fprintf(s,?"(code?%d)?%+v",?e.code,?e.cause)
          ???}?else?{
          ??????//?Simple?mode.?Make?fmt?ask?the?cause
          ??????//?to?print?itself?simply.
          ??????fmt.Fprint(s,?e.cause)
          ???}
          }

          通過此代碼可見,在 Format 內(nèi)部調(diào)用的 fmt.Fprintf 或 fmt.Fprint 的第一參數(shù)是 fmt.State 的實例,這是 fmt 包負(fù)責(zé)注入的。簡單字符串和非錯誤值可以傳遞,每次看到錯誤值時,它都會被”忽略”,以便外部 fmt 循環(huán)可以轉(zhuǎn)到下一層,而不會重復(fù)輸出。

          這個想法的問題, 要知曉 Format() 方法中是怎么使用 fmt.State 的。它不適用于實現(xiàn)以下函數(shù)的軟件包:

          func?(w?*withMessage)?Format(s?fmt.State,?verb?rune)?{
          ????switch?verb?{
          ????????//?...
          ????case?'s',?'q':
          ????????io.WriteString(s,?w.Error())
          ????}
          }

          (這個例子來自 pkg/errors)。

          請注意,與 Go 生態(tài)系統(tǒng)中的許多其他實現(xiàn)一樣,此實現(xiàn)也挫敗了我們的想法:某些打印使用 fmt.State 的 io.Writer 子接口并將 .Error() 字符串直接傳遞給它。當(dāng)包裝器的 Format() 正在打印下一層錯誤時,無法可靠地從 fmt.State 中進(jìn)行檢測,從而捕獲該錯誤以執(zhí)行其他操作。

          因此,Go 生態(tài)系統(tǒng)中“將錯誤作為鏈接列表”的集成與 fmt.Formatter 抽象發(fā)生沖突,并創(chuàng)建了一個坑,社區(qū)中的每個人都陷入困境,而 Go 標(biāo)準(zhǔn)庫無法幫助任何人在 fmt 中使用魔術(shù)。

          也許是救星:pre-1.13 xerrors

          在進(jìn)行 Go 1.13 的工作中,2017 年成立了一個工作組,研究采用“錯誤作為鏈接列表”的方法,并基本上接管了 Dave Cheney 在 pkg/errors 中的工作。

          這就是由 Jonathan Amsterdam,Russ Cox,Marcel van Lohuizen 和 Damien Neil 組成的小組開始開發(fā) xerrors[7] 包,以作為新抽象的原型和研究依據(jù)。

          這項工作指導(dǎo)作者提出了一些建議:

          • Marcel van Lohuizen: Error Printing — Draft Design[8] (August 2018)
          • Jonathan Amsterdam, Russ Cox, Marcel van Lohuizen, Damien Neil: Proposal: Go 2 Error Inspection[9] (January 2019)

          他們的工作主要集中在 Unwrap() 的語義以及新 API error.Is() 和 errors.As() 的創(chuàng)建上,以可靠地從錯誤對象中識別和提取信息。

          Marcel van Lohuizen 更加關(guān)注錯誤處理的打印方面,并設(shè)計了以下提案:

          • 除了 fmt.Formatter,error,fmt.Stringer 和 fmt.GoStringer 外,fmt 包支持一個新接口:errors.Formatter。

          • 新接口將通過錯誤包裝和葉類型實現(xiàn)。

          • 提議的接口如下:

            package?errors

            type?Formatter?{
            ?????error

            ?????//?FormatError?can?be?implemented?to?customize?the?formatting
            ?????//?of?errors,?instead?of?fmt.Formatter's?Format.
            ?????//
            ?????//?It?has?access?to?an?errors.Printer?(see?below)
            ?????//?to?actually?produce?output.
            ?????//
            ?????//?In?the?common?case,?the?code?in?FormatError?details
            ?????//?the?current?layer?and?returns?the?next?error?layer
            ?????//?to?print,?or?`nil`?to?indicate?the?tail?of?the
            ?????//?linked?list?has?been?reached.
            ?????//
            ?????//?Optionally,?the?code?for?a?wrapper's?FormatError
            ?????//?can?take?over?formatting?of?both?itself?*and?all
            ?????//?subsequent?layers*?by?producing?its?custom
            ?????//?representation?for?all?and?then?returning?`nil`,
            ?????//?even?though?its?Unwrap()?method?is?still?used
            ?????//?by?errors.Is()?to?iterate?through?the?tail.
            ?????FormatError(p?Printer)?(next?error)
            }

            type?Printer?interface?{
            ????Print(...)??//?can?be?used?to?output?stuff
            ????Printf(...)?//?can?be?used?to?output?stuff

            ????//?Detail?is?a?“magic”?predicate?which?both?indicates?whether
            ????//?verbose?mode?is?requested?via?%+v,?and?also?starts?indenting
            ????//?the?output?performed?by?subsequent?Print()/Printf()?calls?in
            ????//?the?interface,?so?that?the?details?are?visually?“pushed?to
            ????//?the?right”.
            ????Detail()?bool
            }

          一個示例用法如下所示:

          //?FormatError?implements?the?errors.Formatter?interface.
          func?(e?*myWrapper)?FormatError(p?errors.Printer)?{
          ????p.Print("always")
          ????if?p.Detail()?{
          ???????p.Printf("hidden:?",?e.code)
          ????}
          ????return?e.cause
          }

          使用此代碼,我們將得到以下行為:

          err?:=?errors.New("hello")
          err?=?&myWrapper{cause:?err,?code:?123}
          err?=?&myWrapper{cause:?err,?code:?456}

          fmt.Println(err)?//?simple?mode:?prints?"always:?always:?hello"

          fmt.Printf("%+v\n",?err)
          //?prints:
          //
          //???always:
          //??????hidden:?456
          //???always:
          //??????hidden:?123
          //???hello

          (請注意一些特性:錯誤是從最外層/頭部到最內(nèi)層/尾部打印的,并且在每個前綴之后,細(xì)節(jié)之前插入了冒號)。

          因此,將 fmt 代碼修改為使用新接口的方式是:

          1. 檢測 Format() 方法是否可用。如果是這樣,它被調(diào)用,結(jié)束。
          2. 否則,如果要打印的對象是錯誤,它將對其進(jìn)行迭代:調(diào)用 FormatError()(如果存在)并使用其返回值作為下一次迭代的輸入進(jìn)行迭代。
          3. 當(dāng)錯誤對象上不存在 FormatError() 或返回 nil 時,迭代將停止。
          4. 如果在迭代結(jié)束時仍有 Format() 或 Error() 方法可供調(diào)用,則將調(diào)用該方法以“完成”格式化。

          xerror 原型能夠集中精力僅格式化一層包裝器,而又不知道如何正確地將 Format() 調(diào)用轉(zhuǎn)發(fā)給其他層。

          因此,這是解決上述第二個基本限制的嘗試。

          哎,它根本無法解決第一個基本限制:如果包裝層未實現(xiàn) FormatError(),則 fmt 代碼將僅停止在該級別嘗試,并且在錯誤中進(jìn)一步進(jìn)行任何 FormatError() 或 Format() 定制鏈會被丟棄。

          此外,許多人不喜歡“從前到后”打印錯誤的方式:在對錯誤詳細(xì)信息進(jìn)行故障排除時,開發(fā)人員發(fā)現(xiàn)重要是首先顯示鏈接列表的“最內(nèi)層”(尾部),然后才是“最外層”(頭)。 xerrors 實現(xiàn)不允許這樣做。

          最后,無論如何,所有討論都是沒有爭議的:沒有選擇將 xerror 打印抽象(包括 errors.Formatter,errors.Printer 和相應(yīng)的 fmt 更改)包含在 Go 1.13 中。從 Go 1.16 開始,朝著這個方向進(jìn)行的任何進(jìn)一步工作都被推遲[10],具體另行通知。

          戰(zhàn)略失誤:打破與pkg/errors 的兼容性

          依賴于 Dave Cheney 的 pkg/errors 的社區(qū)項目超過 50,000 個,該軟件包已成為事實上的擴(kuò)展,能夠提供錯誤包裝程序的基本庫,并作為錯誤打印自定義示例,盡管不完善。

          甚至有一個擴(kuò)展的生態(tài)系統(tǒng),它依靠基本的鏈表抽象,使用一種名為 Cause() 的方法來接受鏈中的下一個層次。

          Go 團(tuán)隊可能已經(jīng)接受了這種方法,并且可以在“所有錯誤包裝程序都必須以類似于 pkg/errors 的方式實現(xiàn) Format”的方式進(jìn)行區(qū)分。然后,errors.Is()/errors.As() 可能選擇了 pkg/errors 的 Cause() 抽象。

          遺憾的是,Go 團(tuán)隊選擇了不同的方法名稱:Unwrap()。因此,Go 1.13 發(fā)布后開發(fā)的新一代錯誤包已無法重用 pkg/errors。

          因此,1.13 不僅引入了基本限制;這也阻止了 Go 社區(qū)繼續(xù)可靠地使用 pkg/errors。

          總結(jié):Go error 打印災(zāi)難

          在 2019 年,Go 1.13 采納了 Dave Cheney 的 2015 年建議,將錯誤對象視為鏈表。因此,對 Unwrap() 方法進(jìn)行了標(biāo)準(zhǔn)化,并使用 Is() 和 As() 函數(shù)增強(qiáng)了錯誤包,這些函數(shù)可以從以這種方式構(gòu)造的錯誤中可靠地提取信息。

          不幸的是,fmt 軟件包沒有學(xué)習(xí)如何打印這種錯誤的新形狀,并且可靠地自定義錯誤對象的顯示已變得不可能。

          這是因為與以前的版本一樣,fmt 僅了解 Format(),Error() 和 String(),并且僅在錯誤鏈的頂端或“頭”考慮這些方法。

          如果一個包定義了自定義包裝錯誤類型,但忘記定義了自定義 Format() 方法,則 fmt 將忽略鏈接列表“尾部”中的任何其他 Format() 方法,并且自定義項將丟失。

          此外,只有 Format() 方法可以為“詳細(xì)”和“簡單”格式(%v /%+v)提供不同的實現(xiàn)。在實踐中,以遞歸方式在錯誤鏈尾部調(diào)用進(jìn)一步的自定義方式,幾乎不可能實現(xiàn)包裝錯誤的 Format()。

          簡而言之,錯誤打印的自定義變得容易出錯,并且在 Go 1.13 中基本上不可靠。Go 1.13 中放棄了的關(guān)鍵 Cause() 接口,導(dǎo)致與另一個具有某些人們可以達(dá)成共識的邏輯的程序包 Dave Cheney 的 pkg/errors 不兼容。 Go 團(tuán)隊通過 xerrors 包來嘗試修復(fù) Go 標(biāo)準(zhǔn)庫中的這種情況,實際上并沒有成功解決這些問題,存在重大的新缺陷,最終不令人滿意。

          這就是我們程序員無所適從的方式。

          這是 Go error 打印災(zāi)難,它的懸念留在 Go 1.16 中。

          接下來

          CockroachDB error 庫在錯誤打印方面花費了大量精力。盡管它不能填補(bǔ)所有空白,但確實可以減輕很多的痛苦。

          本系列的下一篇文章進(jìn)一步說明。

          參考文獻(xiàn)

          • Dave Cheney: Don’t just check errors, handle them gracefully[11].
          • The Go Blog: Working with errors in Go 1.13[12].
          • Jonathan Amsterdam, et al: Go 2 error values[13].
          • Marcel van Lohuizen: Error Printing — Draft Design[14].
          • Jonathan Amsterdam, Russ Cox, Marcel van Lohuizen, Damien Neil: Proposal: Go 2 Error Inspection[15].

          原文鏈接:https://dr-knz.net/go-error-printing-catastrophe.html

          本文作者:Raphael ‘kena’ Poss

          譯者:polarisxu

          參考資料

          [1]

          “CockroachDB errors 庫”: https://github.com/cockroachdb/errors

          [2]

          Go 標(biāo)準(zhǔn)錯誤 API: https://dr-knz.net/cockroachdb-errors-std-api.html

          [3]

          標(biāo)準(zhǔn) API: https://en.wikipedia.org/wiki/C_file_input/output

          [4]

          Go 的格式化 API。: https://dr-knz.net/go-formatting-apis.html

          [5]

          errors: https://github.com/cockroachdb/errors

          [6]

          go-mtfwd: https://github.com/knz/go-fmtfwd

          [7]

          xerrors: https://github.com/golang/xerrors

          [8]

          Marcel van Lohuizen: Error Printing — Draft Design: https://go.googlesource.com/proposal/+/master/design/go2draft-error-printing.md

          [9]

          Jonathan Amsterdam, Russ Cox, Marcel van Lohuizen, Damien Neil: Proposal: Go 2 Error Inspection: https://go.googlesource.com/proposal/+/master/design/29934-error-values.md

          [10]

          朝著這個方向進(jìn)行的任何進(jìn)一步工作都被推遲: https://github.com/golang/go/issues/29934#issuecomment-591488854

          [11]

          Dave Cheney: Don’t just check errors, handle them gracefully: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully

          [12]

          The Go Blog: Working with errors in Go 1.13: https://blog.golang.org/go1.13-errors

          [13]

          Jonathan Amsterdam, et al: Go 2 error values: https://github.com/golang/go/issues/29934

          [14]

          Marcel van Lohuizen: Error Printing — Draft Design: https://go.googlesource.com/proposal/+/master/design/go2draft-error-printing.md

          [15]

          Jonathan Amsterdam, Russ Cox, Marcel van Lohuizen, Damien Neil: Proposal: Go 2 Error Inspection: https://go.googlesource.com/proposal/+/master/design/29934-error-values.md



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 83
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  日日日操| 无码一区二区三区四区五区在线看 | 亚洲国产色婷婷 | 国精品人伦一区二区三区蜜桃 | 黄色一级在线观看 |