Go 還是需要泛型的
Go 語言之父早期提到過 less is more[1] 的哲學(xué),可惜社區(qū)里有不少人被帶偏了。
每次 Go 增加語法上的新特性、新功能,就會有原教旨主義者跳出來扔出一句 less is more(或者也可能是大道至簡),揚(yáng)長而去,留下風(fēng)中凌亂的你。
即使到了官方已經(jīng)確定要增加泛型功能的 2020 年,依然有人煞有介事地寫文章說為什么 go doesn't need generics[2],作為理智的 Gopher,最好不要對別人的結(jié)論盡信,至少要看看其它語言社區(qū)怎么看待這件事情。
Java 社區(qū)是怎么理解泛型的必要性的呢?
簡而言之,泛型使類型(類和接口)能夠在定義類、接口和方法時成為參數(shù)。就像我們更熟悉的在方法聲明中使用的形式參數(shù)一樣,類型參數(shù)為你提供了一種用不同的輸入重復(fù)使用相同代碼的方法。不同的是,形式參數(shù)的輸入是值,而類型參數(shù)的輸入是類型。
與非泛型代碼相比,使用泛型的代碼有很多好處。
在編譯時進(jìn)行強(qiáng)類型檢查。Java 編譯器對泛型代碼進(jìn)行強(qiáng)類型檢查,如果代碼違反類型安全就會報錯。編譯時的錯誤比運(yùn)行時的錯誤更易修復(fù)。
消除類型轉(zhuǎn)換。下面這段代碼片段在沒有泛型時,需要類型轉(zhuǎn)換:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
用泛型重寫,代碼不再需要進(jìn)行類型轉(zhuǎn)換:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
程序員可以編寫泛型算法 使用泛型可以實(shí)現(xiàn)在不同類型上都可以工作的泛型算法的同時,保證類型安全性。
Gopher 可能對“類型安全”不太熟悉,舉個例子,我們可以用 gods 庫來實(shí)現(xiàn)下面的數(shù)據(jù)結(jié)構(gòu)。
package main
import (
"fmt"
sll "github.com/emirpasic/gods/lists/singlylinkedlist"
)
func main() {
list := sll.New()
list.Add("a") // ["a"]
}
我們的本意是實(shí)現(xiàn)一個 string 的單鏈表,但是通用的數(shù)據(jù)結(jié)構(gòu)庫沒有辦法阻止用戶向該 list 內(nèi)插入非 string 類型的值,比如用戶可以這樣:
list := sll.New()
list.Add("a") // ["a"]
list.Add(2) // ["a", 2]
這顯然不是我們想要的結(jié)果。
可見泛型最常見的場景是在類型安全的前提下實(shí)現(xiàn)算法流程,對于 Go 來說,我們使用的數(shù)據(jù)結(jié)構(gòu)和算法來源有兩個地方:container 標(biāo)準(zhǔn)庫、第三方數(shù)據(jù)結(jié)構(gòu)庫,如 gods[3] 。
和我們前面舉的例子一樣,標(biāo)準(zhǔn)庫的通用 container 的大多接口也是接收空 interface{},或返回空 interface{}:
package main
import (
"container/list"
)
func main() {
l := list.New()
l.PushBack(4)
l.PushFront("bad value")
}
做不到類型安全的話,那么用戶代碼就可能在運(yùn)行期間發(fā)生斷言產(chǎn)生的 panic/error。除了容器的功能容易被破壞,類似下面的 bug 也挺容易出現(xiàn)的:
package main
import "fmt"
type mystring string
func main() {
var a interface{} = "abc"
var b interface{} = mystring("abc")
fmt.Println(a == b)
}
社區(qū)的其它嘗試
社區(qū)曾經(jīng)有一些靠代碼生成實(shí)現(xiàn)的泛型庫,如genny[4],其本質(zhì)是使用文本替換來實(shí)現(xiàn)多種類型的代碼生成。
genny 使用也比較簡單,比如 example 里的例子:
package queue
import "github.com/cheekybits/genny/generic"
// NOTE: this is how easy it is to define a generic type
type Something generic.Type
// SomethingQueue is a queue of Somethings.
type SomethingQueue struct {
items []Something
}
func NewSomethingQueue() *SomethingQueue {
return &SomethingQueue{items: make([]Something, 0)}
}
func (q *SomethingQueue) Push(item Something) {
q.items = append(q.items, item)
}
func (q *SomethingQueue) Pop() Something {
item := q.items[0]
q.items = q.items[1:]
return item
}
cat source.go | genny gen "Something=string"
// This file was automatically generated by genny.
// Any changes will be lost if this file is regenerated.
// see https://github.com/cheekybits/genny
package queue
// StringQueue is a queue of Strings.
type StringQueue struct {
items []string
}
func NewStringQueue() *StringQueue {
return &StringQueue{items: make([]string, 0)}
}
func (q *StringQueue) Push(item string) {
q.items = append(q.items, item)
}
func (q *StringQueue) Pop() string {
item := q.items[0]
q.items = q.items[1:]
return item
}
想實(shí)現(xiàn)多種類型的結(jié)構(gòu)就在生成代碼時傳入多種類型就可以了。
這種做法和人們調(diào)侃 Go 泛型時使用的 gif[5] 本質(zhì)上也沒什么區(qū)別。
語言的原生支持能讓我們省事,并且也能在實(shí)現(xiàn)上更加嚴(yán)謹(jǐn)。
在 《Rise and Fall of Software Recipes》一書中,有這么一個故事:
Among the recent projects failing because (or despite) of strong processes, Obamacare is a telling example. It involves 50 contractors, has cost fortunes, was delivered late and crippled with bugs. It was developed using a typical waterfall process, and if only because of that, the Agile community started howling, claiming that they would have made the project a success[Healthcare.gov failure].
And when an Agile project fails like Universal Credit in Great-Britain[UniversalCredit] [NAO2013], even when the full report states that the lack of detailed blueprint – typical of Agile methodologies – was one of the factors that caused the failure, common Agile wisdom says it is because it was not applied properly, or should I say, not Agile enough.
簡而言之,就是敏捷大師們其實(shí)非常雙標(biāo),他們給出的方法論也不一定靠譜,反正成功了就是大師方法得當(dāng),失敗了就是我們執(zhí)行不力沒有學(xué)到精髓。正著說反著說都有道理。
再看看現(xiàn)在的 Go 社區(qū),buzzwords 也很多,如果一個特性大師不想做,那就是 less is more。如果一個特性大師想做,那就是 orthogonal,非常客觀。
對于不想迷信大師的 Gopher 來說,多聽聽批評意見沒壞處:go is not good[6]。
less is more: https://en.wikipedia.org/wiki/Less_is_more
[2]go doesn't need generics: https://dzone.com/articles/go-doesnt-need-generics
[3]gods: https://github.com/emirpasic/gods
[4]genny: https://github.com/cheekybits/genny
[5]gif: https://twitter.com/yogthos/status/883058510275149826
[6]go is not good: https://github.com/ksimka/go-is-not-good
