技術雜談:Go (Golang) 是否該支援泛型

PUBLISHED ON JUL 29, 2017

    「Go (golang) 應該要有泛型 (generics)」,這大概是 Go 社群最常拿出來炮的一個議題。筆者有訂閱 Go 社群發行的電子報,也有在推特 (Twitter) 上追蹤 Go 的社群動態,這個議題每隔一陣子就會看到某個部落客發文 (編按:就像我們這篇),或是某個開發者又造出某個非官方的輪子,Go 社群對這個議題的態度不言自明。最近 Go 發布了 2.0 版本的 roadmap,又讓大家燃起一線生機,是否能利用大版本更新的時候將泛型做為 Go 回饋社群的一份大禮呢?

    撰寫泛型程式時,程式設計者不需要預先指明程式的型別,而可以待後續呼叫該程式碼再宣告型別。動態型別 (dynamically-typed) 的語言,例如 Python、Ruby、PHP、Perl 等,不需要泛型,因為程式碼本身沒有限制可用的型別;對於靜態型別 (statcially-typed) 語言,例如 C++、Java、C# 等,來說,使用泛型可以更靈活地重用程式碼。許多有關資料結構 (data structures) 或容器 (collections) 的函式庫,大量地使用泛型。

    對於現代語言來說,泛型應該是程式設計者的共識。老牌的企業用語言 Java 和 C# 雖然一開始不支援泛型,兩者不約而同在後續的主版本中加入泛型的機制,一些新興的語言,像是 TypeScript、Rust 或是 Kotlin,也都加入泛型的機制,即使像是 Dart 這種半靜態型別的程式語言,也支援泛型。大部分程式語言都很有默契,不約而同地在角括號 <> 中塞入型別資訊,做為泛型的語法,角括號似乎成了程式設計者的對泛型的集體潛意識。

    回頭來看 Go 語言,到本文撰寫的時間為止,Go 開發團隊在其官方網站的 FAQ 中明確地指出 Go 不支援泛型。不過,其實 Go 在內建的 array、slice、map 等資料結構中隱微地支援泛型;程式設計師可以對 Go 的這些容器指定任意的型別,Go 也提供數個內建函式來操作這些容器。不過,Go 對泛型的「正式」支援就那麼多而已,使用者自訂的程式碼就沒有這種好康。那麼,Go 程式設計者如何處理泛型議題呢?

    基本上,Go 社群的泛型替代方案,約有以下四種:

    • 介面 (interface)
    • 空介面 (empty interface) 加上反射 (reflection) 或是委派 (delegation)
    • 程式碼生成器
    • 使用轉譯為 Go 的新語言

    要注意的是,一和二是不同的方案,我們將於後文說明。

    使用介面,我們可以預先定義好一些公開方法 (method),函式庫開發者在撰寫程式碼時呼叫這些公開方法,而將實作細節委任函式庫使用者。一個典型的例子是 Sorting by Functions (見 Go by Example 的實例),程式設計者實作 Len()、Swap()、Less() 三個方法,之後即可透過 Go 提供的 Sort 函式來排序。這種方案,將某一部分的責任轉移到函式庫使用者身上,使得 Go 程式設計者常常要寫一些樣板 (boilerplate) 程式碼,而受到一些批評。由於 Go 標準函式庫中即實作這種模式,這種思維應該就是原先 Go 團隊所想到的泛型替代方案。

    至於使用空介面實作泛型則有一些 dirty hack 的味道。由於 Go 的空介面可以塞入任意的型別,這時候,函式庫開發者可以利用反射在執行期 (runtime) 動態檢查變數的型別,再執行相對應的動作,或是將型別檢查的動作委派給函式庫使用者。這樣說明可能過於抽象,筆者先前在學習 Go 時,以此種方式實作了 list 和 vector 等小型容器,讀者可自行前往參考 (algo-golangds-golang)。然而,這種方式沒有成為主流,因為大量使用反射會拖累程式執行的速度,大約慢 10 到 100 倍左右,而委派則和第一個方案大同小異,函式庫使用者仍然需要一些手工。

    使用程式碼生成器,則是將原先編譯器的工作委由程式設計者手動執行,實際的案例可見 genny 這個專案。筆者曾短暫試過這個專案,設計得相當有趣;如果某段程式碼僅在開發過程中需要泛型的特性,但最後的程式碼産出不需泛型的話,倒也可以考慮用此專案減少重覆撰碼的苦工。

    建立新語言則是比程式碼生成器更為激進的方案,筆者日前看到 ply 專案就是採取此種方式。ply 建立一個語法幾乎和 Go 相同的超語言,加上一些泛型函式,再將 ply 程式碼轉換為 Go 程式碼。ply 的思維很有趣,但一個語言的生態圈不是一朝一夕就可以建立的;以 JavaScript 為例,先後有數個新語言試圖改善 JavaScript 本身的問題,但在 TypeScript 出現後,才逐漸取得 JavaScript 社群的共識。基本上,筆者暫時不會去採用這個新專案。

    程式的目的之一就是要將繁瑣的事務自動化,比起這些縫縫補補的 (patchy) 方案,能夠直接從語言機制本身來解決就再好也不過了。筆者就像大部分的 Gophers 般,期待有一天 Go 能夠有一個官方的、正式的泛型解決方案。