Go 社区之父早期提到过 less is more 的哲学,可惜社区里有不少人被带偏了。
每次 Go 增加语法上的新特性、新功能,就会有原教旨主义者跳出来扔出一句 less is more(或者也可能是大道至简),扬长而去,留下风中凌乱的你。
即使到了官方已经确定要增加泛型功能的 2020 年,依然有人煞有介事地写文章说为什么 go doesn't need generics,作为理智的 Gopher,最好不要对别人的结论尽信,至少要看看其它语言社区怎么看待这件事情。
Java 社区是怎么理解泛型的必要性的呢?
简而言之,泛型使类型(类和接口)能够在定义类、接口和方法时成为参数。就像我们更熟悉的在方法声明中使用的形式参数一样,类型参数为你提供了一种用不同的输入重复使用相同代码的方法。不同的是,形式参数的输入是值,而类型参数的输入是类型。
与非泛型代码相比,使用泛型的代码有很多好处。
-
在编译时进行强类型检查。
Java 编译器对泛型代码进行强类型检查,如果代码违反类型安全就会报错。编译时的错误比运行时的错误更易修复。 -
消除类型转换。
下面这段代码片段在没有泛型时,需要类型转换:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
用泛型重写,代码不再需要进行类型转换:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
- 程序员可以编写泛型算法
使用泛型可以实现在不同类型上都可以工作的泛型算法的同时,保证类型安全性。
Gopher 可能对“类型安全”不太熟悉,举个例子,我们可以用 gods 库来实现下面的数据结构。
package main
import (
"fmt"
sll "github.com/emirpasic/gods/lists/singlylinkedlist"
)
func main() {
list := sll.New()
list.Add("a") // ["a"]
}
我们的本意是实现一个 string 的单链表,但是通用的数据结构库没有办法阻止用户向该 list 内插入非 string 类型的值,比如用户可以这样:
list := sll.New()
list.Add("a") // ["a"]
list.Add(2) // ["a", 2]
这显然不是我们想要的结果。
可见泛型最常见的场景是在类型安全的前提下实现算法流程,对于 Go 来说,我们使用的数据结构和算法来源有两个地方:container 标准库、第三方数据结构库,如 gods 。
和我们前面举的例子一样,标准库的通用 container 的大多接口也是接收空 interface{},或返回空 interface{}:
package main
import (
"container/list"
)
func main() {
l := list.New()
l.PushBack(4)
l.PushFront("bad value")
}
做不到类型安全的话,那么用户代码就可能在运行期间发生断言产生的 panic/error。除了容器的功能容易被破坏,类似下面的 bug 也挺容易出现的:
package main
import "fmt"
type mystring string
func main() {
var a interface{} = "abc"
var b interface{} = mystring("abc")
fmt.Println(a == b)
}
社区的其它尝试
社区曾经有一些靠代码生成实现的泛型库,如genny,其本质是使用文本替换来实现多种类型的代码生成。
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
}
想实现多种类型的结构就在生成代码时传入多种类型就可以了。
这种做法和人们调侃 Go 泛型时使用的 gif 本质上也没什么区别。
语言的原生支持能让我们省事,并且也能在实现上更加严谨。
在 《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.
简而言之,就是敏捷大师们其实非常双标,他们给出的方法论也不一定靠谱,反正成功了就是大师方法得当,失败了就是我们执行不力没有学到精髓。正着说反着说都有道理。
再看看现在的 Go 社区,buzzwords 也很多,如果一个特性大师不想做,那就是 less is more。如果一个特性大师想做,那就是 orthogonal,非常客观。
对于不想迷信大师的 Gopher 来说,多听听批评意见没坏处:go is not good。