在昨天的 Go contributor 年度峰會上,與會者對錯誤處理和泛型的設計草案有了一個初步的瞭解。Go 2 的開發項目是去年宣布的,今天Google公布了這一語言的更新。
欲快速瞭解相關內容,請看Google在 Gophercon 2018 上播放的視頻:
作為 Go 2 設計進程的一部分,Google發布了這些設計草案,以激發社區關於以下三個話題的討論:泛型(generics)、錯誤處理和錯誤值語義(error value semantics)。
這些設計草案不算 Go 提案流程意義上的提案。它們只是激發討論的引子,最終目的是給出足夠好的設計並將其轉變為實際提案。每種設計草案都附帶一個「問題概述」,其作用是:(1)提供語境;(2)為包含更多設計細節的實際設計文檔做準備;(3)推動關於設計架構和說明的討論。問題概述會提供背景、目標、非目標、設計約束、設計的簡要總結、對重點關注領域的簡短討論以及與先前方法的比較。
再次重申,這些只是設計草案,不是官方提案。現在沒有相關提案事宜。Google希望 Go 的所有使用者都能夠協助其改進草案並將草案完善為 Go 提案。為此,Google建立了一個 wiki 頁面來收集並組織關於每個話題的反饋。Google希望使用者協助其更新這些頁面,包括添加使用者自己的反饋連結。
簡介
本概覽及附帶的細節草案是《Go 2 設計草案》(Go 2 Draft Designs)文檔的一部分。Go 2 的總體目標是為 Go 無法擴充到大型程式碼程式庫和大量開發人員這一問題提供最重要的解決方式。
Go 編程無法成功擴充的一大原因在於錯誤檢查和錯誤處理代碼的編寫。總體來看,Go 編程代碼檢查錯誤太多,但處理這些錯誤的代碼卻非常不足(下文將給出解釋)。該設計草案旨在通過引入比當前慣用的「賦值和 if 語句」(assignment-and-if-statement)組合更輕量級的錯誤檢查文法來解決這個問題。
作為 Go 2 的一部分,Google還考慮對錯誤值的語義變更,這是一個單獨的關注點,但是本文檔僅涉及錯誤檢查和處理。
在 Go 開源之前,Go 團隊成員——尤其是 Ian Lance Taylor——就一直在研討「泛型」的可能設計(即參數多態,parametric polymorphism)。Google從 C++ 和 Java 的經驗中得知,這一話題非常豐富、複雜,要想考慮透徹並設計出一個良好的解決方案將花費很長時間。Google一開始並沒有嘗試這一做法,而是將時間花在了更直接適用於 Go 網路系統軟體(現在的「雲軟體」)這一初始目標的功能上,例如並發性、可擴充構建和低延遲垃圾收集。
Go 1 發布之後,Google繼續探索泛型的多種可能設計。2016 年 4 月,Google發布了這些早期設計(https://go.googlesource.com/proposal/+/master/design/15292-generics.md#)。作為 Go 2 再次進入「設計模式」的一部分,Go 團隊再次嘗試探索泛型的設計,希望泛型能與 Go 語言融合,為使用者提供足夠的靈活性和表達性。
在 2016 和 2017 年的 Go 使用者調查中,某種形式的泛型是最迫切的兩個功能需求之一(另一個是包管理)。Go 社區維護一份「Go 泛型討論摘要」(Summary of Go Generics Discussions)文檔。
許多人錯誤地以為 Go 團隊的立場是「Go 永遠不會有泛型」。但這並非事實,Google知道泛型的潛力,它能讓 Go 更加靈活、強大、複雜。如果要增加泛型,Google想在盡量不增加 Go 複雜度的前提下努力提高其靈活度,並使其更加強大。
錯誤處理:問題概覽
為了擴充至大型程式碼程式庫,Go 程式必須是輕量級的,沒有不適當的重複,且具備穩健性,能夠優雅地處理出現的錯誤。
在 Go 的設計中,我們有意識地選擇使用顯性的錯誤結果和錯誤檢查。而 C 語言通常主要使用對隱性錯誤結果的顯性檢查,而很多語言(包括 C++、C#、Java 和 Python)中都出現的異常處理表示對隱性結果的隱性檢查。
目標
對於 Go 2,我們想使錯誤檢查更加輕量級,減少用於錯誤檢查的 Go 程式文本量。我們還想更加方便地寫處理錯誤的程式,提高編程人員處理錯誤的可能性。
錯誤檢查和錯誤處理必須是顯性的,即在程式文本中可見。我們不想重複異常處理的缺陷。
現有代碼必須能夠繼續運行,且和現在一樣有效。任何改變都必須能夠實現對現有代碼的互操作。
如前所述,該設計的目標不是改變或增強錯誤的語義。
錯誤值:問題概覽
大程式必須能夠以編程的方式測試錯誤和作出反應,還要報告這些錯誤。
由於錯誤值是實現 error 介面的任意值,Go 程式中有四種測試特定錯誤的傳統方式。一,程式可以使用 sentinel error(如 io.EOF)測試它們的等價性。二,程式能夠使用 Type assertions 或 type switch 檢查錯誤實作類別型。三,點對點檢查(如 os.IsNotExist)檢查特定種類的錯誤,進行有限的解包。四,由於當錯誤被封裝進額外的上下文中時,這些方法通常都不奏效,因此程式通常在 err.Error() 報告的錯誤文本中進行子字串搜尋。很明顯,最後一種方法最不可取,即使是在出現任意封裝的情況下,支援前三種方法更好。
目標
我們有兩個目標,分別對應兩個主要問題。一,我們想使檢查程式錯誤的過程更加簡單,出現的錯誤更少,從而改善錯誤處理和真實程式的穩健性。二,我們想以標準格式列印出具備額外細節的錯誤。
任何解決方案必須能夠使現有代碼正常運行,且適合現有的源樹。尤其是,必須保留使用 error sentinel(如 io.ErrUnexpectedEOF)對比是否相等以及測試特定種類的錯誤這些概念。必須繼續支援現有的 error sentinel,現有代碼不必改變成返回不同錯誤類型。即擴充函數(如 os.IsPermission)來理解任意封裝而不是固定集是可行的。
在考慮列印額外錯誤細節的解決方案時,我們偏好於使用 golang.org/x/text/message 使定位和翻譯錯誤成為可能,或至少避免不可能。
包必須繼續輕鬆定義其錯誤類型。定義新的通用「真實錯誤實現」是不可接受的,且使用這種實現需要所有代碼。對錯誤實現添加很多其他需求也是不可接受的,這些錯誤實現只涉及到幾個包。錯誤還必須能夠高效建立。錯誤並非異常。在程式運行期間,產生、處理、丟棄錯誤都是很平常的事。
很多年前,Google一個用基於異常(exception-based)的語言寫的程式被發現一直產生異常。最後發現,深層嵌套堆棧上的函數嘗試開啟檔案路徑固定列表中的每個路徑去尋找設定檔。每個失敗的開啟操作就會導致一個異常;異常的產生浪費了大量時間記錄這個深層執行堆棧;之後調用器丟棄了所有這些工作,繼續進行迴圈。在 Go 代碼中錯誤的產生必須保持固定的開銷,不管堆棧深度或其他語境如何。(延遲的處理常式在堆棧解開之前運行也是由於同樣的原因:關心堆棧內容相關的處理常式能夠檢查活躍的堆棧,無需昂貴的 snapshot 操作。)
泛型:問題概覽
為了推廣 Go 語言的大型程式碼程式庫和開發人員的貢獻,提高代碼的複用性就顯得非常重要。實際上,Go 語言早期的關注點只是確保能快速構建包含很多獨立軟體包的程式,因此代碼的複用成本並不是很高。Go 語言的關鍵特徵之一是它的介面方式,這種方式同樣也直接定位於提高代碼複用性。具體來說,這種介面可以寫一個演算法的抽象實現,從而消除不必要的細節。例如,container/heap 在 heap.Interface 操作上以普通函數的方式提供了堆維護(heap-maintenance)的演算法,這使得 container/heap 適用於任何備用儲存,而不僅僅只是一些值。介面的這些屬性令 Go 非常強大。
與此同時,大多數希望擷取優先順序序列的編程器並不希望為演算法實現底層儲存,然後再調用堆演算法。這些編程器更願意讓實現自行管理它的數組,但是 Go 不允許以 type-safe 的方式表達它。最接近的是建立 interface{} 值的優先序列,並在擷取每一個元素後使用類型斷言。
多態變成不僅僅是資料容器。我們可能希望將許多通用演算法實現為樸素的函數,它們能應用各種類型,但是我們現在在 Go 中寫的函數都只能應用於單個類型。泛型函數的樣本可能為如下:
// Keys returns the keys from a map.func Keys(m map[K]V) []K// Uniq filters repeated elements from a channel,// returning a channel of the filtered data.func Uniq(<-chan T) <-chan T// Merge merges all data received on any of the channels,// returning a channel of the merged data.func Merge(chans ...<-chan T) <-chan T// SortSlice sorts a slice of data using the given comparison function.func SortSlice(data []T, less func(x, y T) bool)
目標
Google的目標是通過帶有類型參數的參數多態性來解決 Go 語言庫的編寫問題,這些問題抽象出了不必要的類型細節(如上所述)。
除了預料之中的容器類型外,Google還希望能編寫有用的庫來操作任意的 map 和 channel 值,理想的方案是編寫能在 []byte 和 string 值上運算的多態函數。
允許其它類型的參數化並不是Google的目標,例如通過常數值進行參數化等。此外允許多態定義的專有化實現也不是目標,例如使用位元封裝(bit-packing)定義一個通用的 vector<T> 和特定的 vector<bool>。
我們希望能從 C++和 Java 的泛型問題中學習經驗。為了支援軟體工程,Go 語言的泛型必須明確記錄對類型參數的約束,以作為調用者和實現之間的明確強制協議。但調用者不滿足這些約束或實現本身就超出了約束時,編譯器報告明確的錯誤也非常重要。
在沒有棘手的特殊情況和沒有暴露實現細節的前提下,Go 語言裡的多態性必須平滑地適應到環境語言中。例如,將類型參數限制到機器表徵為單個指標或單個詞彙的情況中是不可接受的。還有另一個例子,一旦以上考慮的通用 Keys(map[K]V) []K 函數被初始化為 K=int 和 V=String,它必須和手寫的非泛型函數在語義上同等地處理。特別是,它必須可分配給類型變數 func(map[int]string) []int。
Go 語言中的多態性應該要在編譯時間和運行時實現,因此用於實現策略的決策還可以用於編譯器,並與其它任何編譯器最佳化一視同仁。這種靈活性將解決泛型困境。Go 語言在很大程度上都是一種直觀且易於理解的語言,如果我們要添加多態性,就必須保留這一點。
參考內容:
https://go.googlesource.com/proposal/+/master/design/go2draft.md
https://news.ycombinator.com/item?id=17859963