我喜欢 Go. 常用它实现各种功能(包括在写本文时的这个博客). Go 很实用,但不够好。 不是说它有多差, 只是没那么好而已。
一门编程语言, 也许会用上一辈子, 所以选择的时候要注意。
本文专注于 Go 的各种吐槽。 老生常谈的有之,鲜为人知的也有。
我用 Rust 和Haskell 作为参照 (至少, 我以为, 这俩都很不错)。 本文列出的所有问题, 都有解决方案。
常规编程那么问题来了
我们写代码可以用于许多不同的事情。假如我写了一个函数用来对一列数字求和,如果我可以用该函数对浮点数、整数以及其他任何类型进行求和那该多棒。如果这些代码包含了类型安全并且可以快速的写出用于整型相加、浮点型相加等的独立函数就更完美了。
好的解决方案:基于限制的泛型和基于参数的多态
到目前为止,我遇到的最好的泛型编程系统是rust和haskell所共用的那个。它一般被称作”被限制的类型“。在haskell中,这个系统被称作”type class“。而在Rust中,它被称作”traits“。像这样:
(Rust, version 0.11)
fn id<T>(item: T) -> T { item }(Haskell)
id :: t -> t id a = a在上面这个简单了例子中,我们定义了一个泛型函数id。id函数将它的参数原封不动传回来。很重要的一点是这个函数可以接受任何类型的参数,而不是某个特定的类型。在Rust和haskell中,id函数保留了它参数的类型信息,使得静态类型检查可以顺利工作,并且没有为次在运行期付出任何代价。你可以使用这个函数来写一个克隆函数。
同样,我们可以应用这种方式来定义泛型数据结构。例如:
(Rust)
struct Stack<T>{ items: Vec<T> }(Haskell)
data Stack t = Stack [t]跟上面一样,我们在没有运行期额外消耗的情况下得到完全的静态类型安全。
现在,如果我们想写一个通用的函数,我们必须告诉编译器“这个函数只有在它的所有参数支持这个函数中所用用到的操作时,才有意义”。举个例子,如果我们想定义一个将它的三个参数相加,并返回其和的函数,我们必须告诉编译器:这三个参数必须支持加法运算。就象这样:
(Rust)
fn add3<T:Num>(a:T, b:T, c:T)->T{ a + b + c }(Haskell)
add3 :: Num t => t -> t -> t -> t add3 a b c = a + b + c在上面这个例子中,我们告诉haskell的编译器:“add3这个函数的参数必须是一个Num(算数数类型)“。因为编译器知道一个Num类型的参数支持加法,所以这个函数的表达式可以通过类型检查。在haskell中,这些限制也可应用于data关键字所做的定义中。这是一个可以优雅地定义百分之百类型安全的灵活泛型函数的方式。
go的解决方案:interface{}
Go的普通类型系统的结果是,Go对通用编程的支持很差。
你可以非常轻松的写通用方程。假如你想写一个可以打印被哈希的对象的哈希值。你可以定义一个拥有静态类型安全保证的interface,像这样:
(Go)
type Hashable interface { Hash() []byte } func printHash(item Hashable) { fmt.Println(item.Hash()) }现在,你可以提供给printHash任何Hashable的对象,你也得到静态类型检查。这很好。
但如果你想写一个通用的数据结构呢?让我们写一个简单的链表。在Go里写通用数据结构的惯用方法是:
(Go)
type LinkedList struct { value interface{} next *LinkedList } func (oldNode *LinkedList) prepend(value interface{}) *LinkedList { return &LinkedList{value, oldNode} } func tail(value interface{}) *LinkedList { return &LinkedList{value, nil} } func traverse(ll *LinkedList) { if ll == nil { return } fmt.Println(ll.value) traverse(ll.next) } func main() { node := tail(5).prepend(6).prepend(7) traverse(node) }