機器之心整理,機器之心編輯部。

Go 語言在工業上有非常多的應用,包括分佈式系統和雲計算平臺等。而 Go 語言並行性能高、部署方便和簡單便捷等特性令其在一些應用上超過了 Python,機器之心也曾討論過由 Python 轉向 Go 的 9 大原因。近日在 Go 語言的開發峯會上,谷歌發佈了 Go 2 的設計草案,包括對泛型、錯誤處理和錯誤值語義等發展的討論。

在昨天的 Go contributor 年度峯會上,與會者對錯誤處理和泛型的設計草案有了一個初步的瞭解。Go 2 的開發項目是去年宣佈的,今天谷歌公佈了這一語言的更新。

作爲 Go 2 設計進程的一部分,谷歌發佈了這些設計草案,以激發社區關於以下三個話題的討論:泛型(generics)、錯誤處理和錯誤值語義(error value semantics)。

這些設計草案不算 Go 提案流程意義上的提案。它們只是激發討論的引子,最終目的是給出足夠好的設計並將其轉變爲實際提案。每種設計草案都附帶一個「問題概述」,其作用是:(1)提供語境;(2)爲包含更多設計細節的實際設計文檔做準備;(3)推動關於設計框架和說明的討論。問題概述會提供背景、目標、非目標、設計約束、設計的簡要總結、對重點關注領域的簡短討論以及與先前方法的比較。

再次重申,這些只是設計草案,不是官方提案。現在沒有相關提案事宜。谷歌希望 Go 的所有用戶都能夠幫助其改進草案並將草案完善爲 Go 提案。爲此,谷歌創建了一個 wiki 頁面來收集並組織關於每個話題的反饋。谷歌希望用戶幫助其更新這些頁面,包括添加用戶自己的反饋鏈接。

簡介

本概覽及附帶的細節草案是《Go 2 設計草案》(Go 2 Draft Designs)文檔的一部分。Go 2 的總體目標是爲 Go 無法擴展到大型代碼庫和大量開發人員這一問題提供最重要的解決方式。

Go 編程無法成功擴展的一大原因在於錯誤檢查和錯誤處理代碼的編寫。總體來看,Go 編程代碼檢查錯誤太多,但處理這些錯誤的代碼卻非常不足(下文將給出解釋)。該設計草案旨在通過引入比當前慣用的「賦值和 if 語句」(assignment-and-if-statement)組合更輕量級的錯誤檢查語法來解決這個問題。

作爲 Go 2 的一部分,谷歌還考慮對錯誤值的語義進行更改,這是一個單獨的關注點,但是本文檔僅涉及錯誤檢查和處理。

在 Go 開源之前,Go 團隊成員——尤其是 Ian Lance Taylor——就一直在研討「泛型」的可能設計(即參數多態,parametric polymorphism)。谷歌從 C++ 和 Java 的經驗中得知,這一話題非常豐富、複雜,要想考慮透徹並設計出一個良好的解決方案將花費很長時間。谷歌一開始並沒有嘗試這一做法,而是將時間花在了更直接適用於 Go 網絡系統軟件(現在的「雲軟件」)這一初始目標的功能上,例如併發性、可擴展構建和低延遲垃圾收集。

Go 1 發佈之後,谷歌繼續探索泛型的多種可能設計。2016 年 4 月,谷歌發佈了這些早期設計(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 永遠不會有泛型」。但這並非事實,谷歌知道泛型的潛力,它能讓 Go 更加靈活、強大、複雜。如果要增加泛型,谷歌想在儘量不增加 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 使定位和翻譯錯誤成爲可能,或至少避免不可能。

包必須繼續輕鬆定義其錯誤類型。定義新的通用「真實錯誤實現」是不可接受的,且使用這種實現需要所有代碼。對錯誤實現添加很多額外要求也是不可接受的,這些錯誤實現只涉及到幾個包。錯誤還必須能夠高效創建。錯誤並非異常。在程序運行期間,生成、處理、丟棄錯誤都是很平常的事。

很多年前,谷歌一個用基於異常(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(

// Merge merges all data received on any of the channels,

// returning a channel of the merged data.

func Merge(chans ...

// SortSlice sorts a slice of data using the given comparison function.

func SortSlice(data []T, less func(x, y T) bool)

目標

谷歌的目標是通過帶有類型參數的參數多態性來解決 Go 語言庫的編寫問題,這些問題抽象出了不必要的類型細節(如上所述)。

除了預料之中的容器類型外,谷歌還希望能編寫有用的庫來操作任意的 map 和 channel 值,理想的方案是編寫能在 []byte 和 string 值上運算的多態函數。

允許其它類型的參數化並不是谷歌的目標,例如通過常數值進行參數化等。此外允許多態定義的專有化實現也不是目標,例如使用比特包裝(bit-packing)定義一個通用的 vector 和特定的 vector

我們希望能從 C++和 Java 的泛型問題中學習經驗。爲了支持軟件工程,Go 語言的泛型必須明確記錄對類型參數的約束,以作爲調用者和實現之間的明確強制協議。但調用者不滿足這些約束或實現本身就超出了約束時,編譯器報告明確的錯誤也非常重要。

在沒有棘手的特殊情況和沒有暴露實現細節的前提下,Go 語言裏的多態性必須平滑地適應到環境語言中。例如,將類型參數限制到機器表徵爲單個指針或單個詞彙的情況中是不可接受的。還有另一個例子,一旦以上考慮的通用 Keys(map[K]V) []K 函數被初始化爲 K=int 和 V=String,它必須和手寫的非泛型函數在語義上同等地處理。特別是,它必須可分配給類型變量 func(map[int]string) []int。

Go 語言中的多態性應該要在編譯時和運行時實現,因此用於實現策略的決策還可以用於編譯器,並與其它任何編譯器優化一視同仁。這種靈活性將解決泛型困境。Go 語言在很大程度上都是一種直觀且易於理解的語言,如果我們要添加多態性,就必須保留這一點。

相關文章