这是一个“ Go不好 ”系列的额外文章。Go确实有一些不错的特性,也就是本文中“好的”部分,但是当我们不使用API或者网络服务器(这是为它设计的)而且将它用于业务领域逻辑的时候,总体而言我发现它用起来笨重且糟糕。但是即使在用于网络编程的时候,在设计和实现方面它也有很多缺陷,这导致它在显而易见的简单的表面之下是危险的。
促使我写这篇文章的原因就是最近我又开始用Go做一个副项目。在我之前的工作我广泛地使用Go来写网络代理(包括http和原生tcp)来做SaaS服务。网络部分相当不错(当时我也是初次尝试这个语言),但是账户和账单部分给我带来了痛苦。由于我的副项目做的是一个简单的API,我觉得Go应该是可以快速完成这个工作的合适的工具,但是就像我们知道的,很多项目会扩张并超过他们的初始范围,因此我不得不写一些数据处理来做统计,然后使用Go就又变得痛苦了。因此下面是我对Go的问题的看法。
一些背景:我喜欢静态类型语言。我的第一个重要项目是用 Pascal 编写的。在90年代初我开始工作之时,我使用了 Ada 和C/C ++。后来我迁移到了Java,最后又使用了Scala(在期间还用过Go),最近开始学习 Rust 。我还写了大量的JavaScript代码,因为直到最近它是Web浏览器中唯一可用的语言。对动态类型语言我感觉不牢靠,并尝试将其应用限制在简单脚本中。我对命令式、函数式和面向对象的方法感到很满意。
这是篇长文,所以,这是让你开胃口的菜单目录:
好的
Go易于学习
易于并发编程的 协程(goroutines )和通道(channels)
强大的标准库
高性能GO
程序语言定义的源代码格式
Defer声明,避免忘记清理
新类型
不好之处
GO忽略现代语言设计的进步
接口是结构类型
没有枚举
:= / var的困境
(让人)恐慌的 零值
Go没有异常
烂的
依赖关系管理的噩梦
用语言硬编码的 可变性
Slice陷阱
可变性和渠道:使竞态条件(race conditions)很容易
混乱的错误管理
Nil接口值
Struct字段标记:字符串中的运行时DSL
没有泛型…至少不是为了你
Go在slice和map之外几乎没有什么数据结构
go generate: ok-ish,但是…
结语
这是事实:如果你会任何一种编程语言,你可以通过“ Go教程 ”在几个小时之内学会Go的大部分语法,在几天之内就可以写出你的第一个程序。阅读和消化 Effective Go ,徘徊在 标准库 中,运用web工具包如 Gorilla 或者 Go kit ,你就能成为一个相当不错的Go开发者。
这是因为Go的首要目标就是简单。当我开始学习Go的时候,它让我回忆起了我初次 接触Java :一个丰富却不臃肿的简单语言。与现在的Java繁重的环境对比,学习Go是一个新鲜的体验。由于Go的简单,Go程序是非常易读的,即使错误处理方面有不少麻烦(这下面更多)。
但是这可能并不是真的简单。引用 Rob Pike的话, 简单即复杂 ,我们在下面可以看到在后面有很多的陷阱等着我们,简洁和极简主义阻止了我们编写DRY原则的代码。
Goroutines可能是Go的最好的特性。与操作系统线程不同,他们是轻量级的计算线程。
当一个Go程序执行阻塞I/O操作一类的工作时,实际上Go实时挂起了这个goroutine,而且在一个event表明一些结果已经可以访问之后,会重新运行。在此期间,其他goroutines已经在为执行调度。因此我们在使用一个同步编程模型做异步编程的时候有可扩展性的优点。
Goroutines也是轻量级的:他们的栈 按需增加或减少 ,也就是说有数百个甚至数千个goroutines都不是问题。
在一个应用中我曾经有一个goroutine泄露:在结束之前这些goroutines等待一个channel去关闭,但那个channel不会关闭(一个常见的死锁问题)。这个进程平白占了90%的CPU,查看 expvars 显示60万个空的goroutine!我猜CPU都被goroutine调度占用了。
当然,一个像Akka的actor系统可以不费力气就 处理数百万actors ,一部分是因为actors没有栈,但是他们在写复杂并发request/response应用(如 http APIs)时不如goroutine简单的多。
Channels是goroutines之间交互的通道:他们提供了一个方便的编程模型可以在goroutines之间发送和接收数据,而不用依赖脆弱的底层同步原语。Channels拥有他们自己的一套 使用 模式 。
由于错误的channels数量(他们默认无缓冲) 会导致死锁 ,Channels必须要慎重考虑。我们在下面也会提到因为Go缺少不变性,使用channels并不能阻止争抢资源。
Go 标准库 真的很强大,特别是对网络协议相关的所有东西或者API开发:http 客户端和服务器,加密,压缩格式,压缩,发送邮件等等。甚至还有html解析器和相当强大的模板引擎,通过自动escaping可以用来产生文字&html来避免XSS(在 Hugo 模板的示例中使用)。
大多数情况下APIs通常是简单易懂的。尽管有时候他们看起来过于简单:这当中,一部分是由于goroutine编程模式告诉我们只需要关心“看似同步”的操作。另一部分是因为少数通用的多功能函数能替代大量单一功能的函数,就像最近一段时间, 我发现的那些用于时间计算的函数 一样。
Go编译成一个本地可执行文件。许多Go的用户来自于Python,Ruby或者Node.js。对他们来说,这是个令人兴奋的体验,因为他们发现服务器可以处理的并发请求数量大幅的增加。对于没有并发的语言(Node.js)或者全局解释器锁(GIL)来说,这实际上是再正常不过的事情。结合语言的简单性,这说明了Go语言令人兴奋的一面。
然而相比Java,在 原始性能基准测试 中,情况并不是那么清晰。 在 内存使用和垃圾收集方面 Go力压Java。
Go的垃圾收集的设计目的是 优先考虑延迟 和避免stop-the-world停顿,这在服务器中尤其重要。这可能会带来更高的CPU成本,但是在水平可伸缩的架构(horizontally scalable architecture)中通过添加更多的机器这是易于解决的。记住,Go是Google设计的,他们不缺资源。
相比于Java,Go的GC在 要做的工作方面 也 更少的: slice的结构 是一个连续的结构数组,而不是像Java这样的指针数组。相似地,Go的maps出于同样的目的使用 像桶的小数组 。这意味着在GC上工作量更少,并且还更有利于CPU的缓存位置。
Go也可以 力压 Java的命令行实用程序:一个本地可执行的,Go程序对Java的首先必须加载和编译字节码来说没有启动成本。
在我职业生涯中一些最激烈的争论发生在团队代码格式的定义上。Go通过为Go代码定义规范格式解决了这个问题。 gofmt 工具会重新格式化你的代码,并且没有选项。
不管喜不喜欢,gofmt都定义了Go代码应该如何格式化,因此该问题得到一次性解决!
Go在其标准库中提供了一个很好的 测试框架 。它支持并行测试、基准测试,并且包含很多用于轻松测试网络客户端和服务器的使用程序。