设为首页 加入收藏

TOP

为什么Go语言不是想象中的那么好(一)
2015-02-02 14:36:24 来源: 作者: 【 】 浏览:27
Tags:为什么 语言 不是 想象 那么

我喜欢 Go. 常用它实现各种功能(包括在写本文时的这个博客). Go 很实用,但不够好。 不是说它有多差, 只是没那么好而已。


一门编程语言, 也许会用上一辈子, 所以选择的时候要注意。


本文专注于 Go 的各种吐槽。 老生常谈的有之,鲜为人知的也有。


我用 Rust 和Haskell 作为参照 (至少, 我以为, 这俩都很不错)。 本文列出的所有问题, 都有解决方案。


go语言 golang


那么问题来了


我们写代码可以用于许多不同的事情。假如我写了一个函数用来对一列数字求和,如果我可以用该函数对浮点数、整数以及其他任何类型进行求和那该多棒。如果这些代码包含了类型安全并且可以快速的写出用于整型相加、浮点型相加等的独立函数就更完美了。


好的解决方案:基于限制的泛型和基于参数的多态


到目前为止,我遇到的最好的泛型编程系统是rust和haskell所共用的那个。它一般被称作”被限制的类型“。在haskell中,这个系统被称作”type class“。而在Rust中,它被称作”traits“。像这样:


(Rust, version 0.11)


(Haskell)


在上面这个简单了例子中,我们定义了一个泛型函数id。id函数将它的参数原封不动传回来。很重要的一点是这个函数可以接受任何类型的参数,而不是某个特定的类型。在Rust和haskell中,id函数保留了它参数的类型信息,使得静态类型检查可以顺利工作,并且没有为次在运行期付出任何代价。你可以使用这个函数来写一个克隆函数。


同样,我们可以应用这种方式来定义泛型数据结构。例如:


(Rust)


(Haskell)


跟上面一样,我们在没有运行期额外消耗的情况下得到完全的静态类型安全。


现在,如果我们想写一个通用的函数,我们必须告诉编译器“这个函数只有在它的所有参数支持这个函数中所用用到的操作时,才有意义”。举个例子,如果我们想定义一个将它的三个参数相加,并返回其和的函数,我们必须告诉编译器:这三个参数必须支持加法运算。就象这样:


(Rust)


(Haskell)


在上面这个例子中,我们告诉haskell的编译器:“add3这个函数的参数必须是一个Num(算数数类型)“。因为编译器知道一个Num类型的参数支持加法,所以这个函数的表达式可以通过类型检查。在haskell中,这些限制也可应用于data关键字所做的定义中。这是一个可以优雅地定义百分之百类型安全的灵活泛型函数的方式。


go的解决方案:interface{}


Go的普通类型系统的结果是,Go对通用编程的支持很差。


你可以非常轻松的写通用方程。假如你想写一个可以打印被哈希的对象的哈希值。你可以定义一个拥有静态类型安全保证的interface,像这样:


(Go)


现在,你可以提供给printHash任何Hashable的对象,你也得到静态类型检查。这很好。


但如果你想写一个通用的数据结构呢?让我们写一个简单的链表。在Go里写通用数据结构的惯用方法是:


(Go)


发现什么了吗?value的类型是interface{}。interface{}就是所谓的“最高类型”,意味着所有其他的类型都是interface{}的子类型。这大致相当于Java中的Object。呀!(注意:对于Go中是否有最高类型还有争议,因为Go宣称没有子类型。不管这些,保留类比的情况。


在Go里面“正确”构建通用数据结构的方法是将对象设置为最高类,然后把它们放入到数据结构中。大约在2004年,Java就是这么做的。后来人们发现这完全违背了类型系统的本意。当你有这样的数据结构时,你完全消除了一个类型系统能提供的所有好处。比如,下面这个是完全有效的代码:


而这在一个良好结构化的程序里完全没有意义。你可能期望的时一个整数链表,但在某个情况下,一些疲惫、靠咖啡清醒的程序员在截止日期前偶然在某处加入了一个字符串。因为Go里面的 通用数据结构不知道它们值的类型,Go的编译器也不会改正,你的程序在你失去从interface{}里面捕获时将崩溃。


相同的问题在任何通用数据结构里都存在,无论是list、map、graph、tree、queue等。


高级语言通常有复杂任务的关键字和符号简写。比如,在很多语言中,迭代一个如数组一样的数据集合中所有元素的简写:


(Java)


(Python)


如果我们能在任何集合类型上操作,不仅仅是那些语言内建类型(如数组),会很美好。


如果我们可以定义类型的相加也会很美好,那么我们可以这么做


(Python)


point3 = point1 + point2


好的解决方案:把运算符视作函数


将内建的运算符和某个特别命名的函数对应起来,亦或将关键字视作特定函数的别名,这样做可以很好的解决该问题。


某些编程语言,像Python,Rust和Haskell允许我们重载运算符。我们只需要给我们自定义的类添加一个函数,自此,当我们使用某个运算符的时候(例如”+“),解释器(编译器)就会直接调用我们所添加的函数。在Python中,运算符”+“对应于__add__()函数。在Rust中,”+“运算符在Add这个trait中定义为add()函数。在Haskell中,”+“对应于Num这个type class中的(+)。


许多语言都有扩展关键字的方法,例如for-each循环。Haskell没有循环,但是像Rust,Java和Python这样的语言中都有”迭代器“这样的概念使得for-each循环可以应用于任何种类的数据集合结构。


某些人可能会用这个特性做一些很操蛋的事情,这是一个潜在的缺点。例如,某些疯狂的家伙使用”-“来代表两个向量之间的点乘。但这并不完全是运算符重载的问题。无论使用何种语言,都可以写出胡乱命名的函数。


Go的解决方案:没有


Go语言不支持操作符重载或者关键字扩展。


那么如果我们想给其他的东西(例如树,链表)实现range关键字的操作怎么办?太糟糕了。这不是语言的一部分。你这能在内建对象上使用range关键字。对于关键字make也一样,它不能给非内建数据结构申请内存和初始化。


最接近这个可以使用迭代器的关键字的方式是写一个包装函数,这个函数以目标数据结构为参数并返回一个可迭代的对象,我们通过使用这个对象在目标数据结构上迭代(译者注:参见设计模式中的迭代器模式或C++中的迭代器实现)。但是这样做可能会很慢并且复杂,而且无法保证不引入其他的bug。


对于这样一个问题,有人辩解道,“这样更容易让人理解代码,并且我看到的代码就是真正被执行的代码。”也就是说,如果Go语言允许我们扩展像range这样的东西,那么range本身的机制和实现就会变得复杂难以理解。我认为这样的说法没有什么营养,因为不管Go是否通过这种方式让其变得更简单,更易懂,人们总要进行这种在某些数据结构上进行迭代操作。如果我们不想把实现细节隐藏在range()函数里,我们就要把它隐藏在其他的工具函数里,没什么改进。所有的好代码都是易读的,大多数糟糕代码让人很难懂,很显然Go不能改变这个事实。


那么问题来了


当遇到递归的数据结构(如链表和树)时,我们希望找到一个途径来指出我们到达数据结构的末端。


当遇到可能会执行失败的函数或包含缺失数据片的数据结构时,我们希望找到一个途径明示我们遇到的几种失败情况。


这回我先说 Go 的, 才好引出其他更好解决方案的讨论.


Go 支持 null 指针(nil). 每次看到新的编程语言(如:tabula rasa), 实现这个导致 b

首页 上一页 1 2 3 下一页 尾页 1/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇Android 动画之属性动画 下一篇经典(Java版)排序算法的分析及..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: