<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 語言的 Bug 價值十億美元

          共 5393字,需瀏覽 11分鐘

           ·

          2020-12-11 10:35

          原文:Billion-Dollar Mistake in Go ?

          地址:https://hackernoon.com/billion-dollar-mistake-in-go-ll1s3tkc?

          作者:Harri Lainio(@lainio[1])?

          翻譯:Jayce Chant(博客:jaycechant.info,公眾號 ID:jayceio)

          十億美元(billion dollar)的錯誤 / bug 貌似是美國的一個梗,大概的意思是,對于那些市值上幾千億的大企業(yè),如果一個錯誤能夠導致市值下跌個百分之零點幾,就已經(jīng)是十億左右了。

          在計算機領域,最著名的 BDM 大概是 圖靈獎得主 Tony Hoare 說他在 1965 年發(fā)明的 null 引用。

          但我不確定這是不是最早的出處,畢竟在商業(yè)領域這樣的說法也很常見。

          以下為譯文:

          以下示例代碼來自 Go 的 標準庫文檔[2]

          data?:=?make([]byte,?100)
          count,?err?:=?file.Read(data)
          if?err?!=?nil?{
          ????log.Fatal(err)
          }
          fmt.Printf("read?%d?bytes:?%q\n",?count,?data[:count])

          代碼看起來沒什么問題。出自標準庫官方文檔的代碼,肯定不會錯,對吧。

          在閱讀介紹 Read 函數(shù)的 io.Reader 文檔[3] 之前,我們先花幾秒鐘來弄清楚這里面有什么問題。

          例子里的 if 語句(至少)應該這樣寫:

          if?err?!=?nil?&&?err?!=?io.EOF?{

          你也許在想,我是不是在自欺欺人:我們不是應該查看 File.Read 函數(shù)的 文檔[4] 嗎?那個才是正確的文檔吧?是的,但那不應該是唯一正確的文檔。

          譯者注:讀到這里的朋友可能會云里霧里,又未必愿意 / 方便(特別是公眾號不能外鏈)看完文檔再回來。我簡單介紹一下。

          io.Reader 接口的文檔里,當 Read 遇到文件結束時,io.EOF 可能跟著非 0 的 n (讀取的有效字節(jié)數(shù))一起返回,也可能在下次調用跟 n = 0 一起返回。(這部分文檔很長,有 1300 多個單詞,還介紹了 Read 方法其它可能的行為,但多數(shù)是建議而不是強制的口吻。)

          File.Read 的文檔則只有一句話,非常明確地指出遇到文件結尾時,會返回 0, io.EOF 。(換言之,io.EOF 不會跟有效字節(jié)一起返回。)

          如果我們不能真的用接口隱藏實現(xiàn)細節(jié),那接口有什么用處?一個接口應該規(guī)定(set)它的語義,而不是像 File.Read 那樣規(guī)定它的實現(xiàn)者。當接口的實現(xiàn)者是 File 以外的其他東西,但仍是一個 io.Reader 時,上面的代碼會發(fā)生什么?當它把數(shù)據(jù)和 io.EOF 一起返回時,它退出得太早了,但這對所有的 io.Reader 實現(xiàn)者都是允許的。

          接口(Interface) vs 實現(xiàn)者(Implementer)

          在 Go 里面,你不需要顯式標記接口的實現(xiàn)者。這是一個強大的特性。但這是否意味著我們總是應該根據(jù)靜態(tài)類型來使用接口語義呢?例如,下面的 Copy 函數(shù)是否應該使用 io.Reader 的語義?

          func?Copy(dst?Writer,?src?Reader)?(written?int64,?err?error)?{
          ????src.Read()?//?現(xiàn)在?read?的語義是來自?io.Reader?嗎?
          ????...
          }

          那這個版本是不是應該只使用 os.File 的語義呢?(注意,這些只是虛構的例子)

          func?Copy(dst?os.File,?src?os.File)?(written?int64,?err?error)?{
          ????src.Read()?//?那現(xiàn)在?read?的語義是不是來自?os.File?的?Read?函數(shù)呢??
          ????...
          }

          實踐中認為,總是應該使用接口語義,而不是綁定到具體的實現(xiàn)——這就是有名的 松耦合[5]。

          io.Reader 的問題

          這個接口有以下問題:

          • 如果不學習 io.Reader 的文檔,你就不能安全地使用任何 Read 函數(shù)的實現(xiàn)。
          • 如果不仔細研究 io.Reader 的文檔,你就無法實現(xiàn) Read 函數(shù)。
          • 由于缺少對錯誤(error)的分類(distinction),接口不夠直觀、完整和符合習慣。

          正因為 io.Reader 是一個接口,前面提到的問題才多了起來。這給 io.Reader 的每個實現(xiàn)者 和 Read 函數(shù)的每個調用者之間帶來了跨包依賴。

          標準庫本身就有很多其它 `io.Reader`[6] 的調用者誤用(misuse)該接口的例子。

          根據(jù)這個 問題單(issue)[7],標準庫——尤其是里面的測試——都堅持使用 if err != nil 這個寫法,這就阻止了 Read 實現(xiàn)中的優(yōu)化。

          例如,當檢測到 io.EOF 時,如果(連同剩余的數(shù)據(jù))立即返回 io.EOF ,就會讓一部分調用者無法正確運行。原因是顯而易見的。reader 接口文檔允許兩種不同類型的實現(xiàn):

          Read 在成功讀取 n > 0 個字節(jié)后,如果遇到錯誤或文件結束的情形,它會返回讀取的字節(jié)數(shù)。它可能會在同一個調用中返回(非 nil)錯誤,也可能會在后續(xù)調用中返回錯誤(同時 n = 0)。

          接口應該是直觀的、并且是通過編程語言本身正式地定義的,使得你無法實現(xiàn)或者誤用它們(cannot implement or misuse them)。開發(fā)者不應該需要先閱讀文檔才能進行必要的錯誤傳遞。

          譯者注:這里的 'cannot implement' 感覺意思不對,不知道原作者是不是想表達錯誤實現(xiàn)的意思,卻只在 use 上加了 mis,忘了 implement。個人猜測本意是 'cannot implement or use them in a wrong way' ,不能錯誤地實現(xiàn)或者使用它們。但這只是我個人的猜測,寫在這里,譯文還是忠實于原文。

          允許接口函數(shù)有多個(本例中是兩個)不同的顯式行為是有問題的。接口的思想,是隱藏實現(xiàn)細節(jié),實現(xiàn)松散耦合。

          最明顯的問題是,io.Reader 接口既不直觀,也不符合 Go 典型的錯誤處理慣例。它還打亂了程序推導中正常和錯誤分離的控制路徑。這個接口使用錯誤傳遞機制來處理一些實際上不是錯誤的東西:

          EOFRead 沒有更多輸入時返回的錯誤。函數(shù)應該只返回 EOF 來表示輸入的正常(grateful)結束。如果 EOF 在結構化數(shù)據(jù)流中意外發(fā)生,相應的錯誤應該是 ErrUnexpectedEOF 或其他能給出更多細節(jié)的錯誤。

          作為可辨識聯(lián)合(Discriminated Unions[8])的錯誤

          io.Reader 接口和 io.EOF 指出了 Go 目前的錯誤處理中所缺少的東西,那就是 錯誤的分類(the error distinction)。例如,Swift 和 Rust 不允許部分失敗。函數(shù)調用要么成功,要么失敗。這就是 Go 的錯誤返回值的問題之一。編譯器無法提供任何支持。眾所周知,這同樣也是 C 語言的非標準錯誤返回的問題——當你有一個重疊的錯誤返回通道時就會這樣。

          Herb Shutter(譯者注:C++ 程序設計專家,曾擔任 ISO C++ 的秘書和會議召集人,原文有筆誤,應為 Sutter)特意在他的 C++ 提案《零開銷的確定性異常:拋出值(Zero-overhead deterministic exceptions: Throwing values)[9]》中提到:

          『正?!慌c 『錯誤』(控制流)是一個非常基礎的語義區(qū)分,而且可能在任何編程語言中都是最重要的區(qū)分,盡管這一點總是被低估。

          解決辦法

          Go 當前 io.Reader 接口存在問題,是因為違反了語義的區(qū)分。

          增加語義上的區(qū)別

          首先,我們通過聲明一個新的接口函數(shù),停止使用返回錯誤來處理不是錯誤的東西。

          Read(b?[]byte)?(n?int,?left?bool,?err?error)

          只允許明顯的行為

          其次,為了 避免混淆 以及 阻止明確的錯誤,我們引導使用下面的助手包裝器(helper wrapper)來處理這兩種允許的 EOF 行為。包裝器只提供了一個顯式行為來處理數(shù)據(jù)的結束。因為文檔中說,必須允許在沒有任何錯誤(包括 EOF)的情況下返回零字節(jié)(不鼓勵在無錯誤的情況下返回零字節(jié)),所以我們不能將讀取的零字節(jié)作為 EOF 的標志。當然,包裝器也保持了錯誤的區(qū)分。

          type?Reader?struct?{
          ????r???io.Reader
          ????eof?bool
          }

          func?(mr?*MyReader)?Read(b?[]byte)?(n?int,?left?bool,?err?error)?{
          ????if?mr.eof?{
          ????????return?0,?!mr.eof,?nil
          ????}
          ????n,?err?=?mr.r.Read(b)
          ????mr.eof?=?err?==?io.EOF
          ????left?=?!mr.eof
          ????if?mr.eof?{
          ????????err?=?nil
          ????????left?=?true
          ????}
          ????return
          }

          我們做了一個錯誤區(qū)分規(guī)則,錯誤和成功的結果是排他的。我們也對返回值 left 進行了區(qū)分。當我們已經(jīng)讀取了所有的數(shù)據(jù),我們會將其設置為 false,使得函數(shù)變得更加易用,這在下面的 for 循環(huán)中可以看到:只有在 left 設為 true ,即數(shù)據(jù)可用時,才需要處理傳入的數(shù)據(jù)。

          for?n,?left,?err?:=?src.Read(dst);?err?==?nil?&&?left;?n,?left,?err?=?src.Read(dst)?{
          ????fmt.Printf("read:?%d,?data?left:?%v,?err:?%v\n",?n,?left,?err)
          }

          正如示例代碼所示,它允許將正常路徑(happy path)和錯誤控制流分開,這使得程序推導變得更加容易。我們在這里展示的解決方案并不完美,因為 Go 的多個返回值之間并無區(qū)別。

          在我們這里,它們都應該是這樣的。無論如何,我們已經(jīng)了解到,每個新人(包括剛接觸 Go 的人)都可以在沒有文檔或示例代碼的情況下使用我們新的 Read 函數(shù)。這就是一個很好的例子,說明 正常路徑和錯誤路徑的語義區(qū)分是多么重要。

          結論

          我們可以說 io.EOF 是一個錯誤(mistake)嗎?我想說是的。這里有一個錯誤(errors)應該與預期的返回(expected returns)區(qū)分開的完美的理由。我們應該始終構建 鼓勵正確路徑(praise happy path)和 防止錯誤[10] 的算法。

          Go 的錯誤處理實踐還缺少語言特性來幫助語義的區(qū)分。幸運的是,我們大多數(shù)人已經(jīng)在清楚區(qū)分的控制流中處理錯誤。

          原譯者文章

          參考資料

          [1]

          @lainio: https://hackernoon.com/u/lainio

          [2]

          標準庫文檔: https://golang.org/pkg/os/

          [3]

          文檔: https://golang.org/pkg/io/#Reader

          [4]

          文檔: https://golang.org/pkg/os/#File.Read

          [5]

          松耦合: https://zh.wikipedia.org/wiki/%E6%9D%BE%E8%80%A6%E5%90%88

          [6]

          io.Reader: https://golang.org/pkg/io/#Reader

          [7]

          問題單(issue): https://github.com/golang/go/issues/21852

          [8]

          Discriminated Unions: https://en.wikipedia.org/wiki/Disjoint_sets

          [9]

          零開銷的確定性異常:拋出值(Zero-overhead deterministic exceptions: Throwing values): http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf

          [10]

          防止錯誤: https://github.com/97-things/97-things-every-programmer-should-know/blob/master/en/thing_66/README.md



          推薦閱讀


          福利

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

          瀏覽 78
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  日韩欧美手机在线观看 | 99精品视频16在线免费观看 | 久久国产乱子伦免费精品 | 人人草人人入 | 国产黄色电影网 |