时间过得真是太快,一晃眼golang都五周岁了。五年,对于人生来讲也许有些漫长,但对于一门语言的发展来说,仅仅是沧海一粟。相对于它试图挑战的「系统级」语言:c,c++,甚至java,golang还年轻得很,就像马拉松比赛,别人都已经跑完了半马,golang才刚刚越过起点线。拿最为人诟病的GC说事 —— 和java的GC比起来,golang的粗糙得像个玩具。按照golang的roadmap,1.5版本release后,也就是明年7月份,按官方的原话来说,golang的GC才会有一个 "making Go acceptable for implementing a broad spectrum of systems requiring low response times" [1] 的版本。注意这里的措辞,是acceptable,不是great,更不是brilliant。

年轻还意味着不成熟。去年我在为别人做的一个兼职项目中,无意中发现了go1.2的编译器对全局变量处理不当(未使用bss段)导致的可执行文件过大的问题 [2]。连我这种新手都能发现golang的问题,可见其成熟度还有待市场的检验。

年轻加上不够成熟,就意味着敢于尝鲜的人不多。尽管有google在为其摇旗呐喊,有docker这样大红大紫的开源项目不断背书,golang的使用状况依旧堪忧:如果你查看11月份的TIOBE指数 [3],golang可怜巴巴地排在了41名,还不及Haskell这样「小众」的函数式编程语言。当然,TIOBE未必反应真实的数据,像我这样整个11月没有搜索过任何有关golang内容的人 [4],恐怕是被排除在golang用户之外的。

说了golang的不好,再说说它的好。

语言上的特性,如concurrency support,messaging with channel,type inference,implicit interface,build into one giant executable等等就不必重复,仁者见仁,淫者见淫。这些特性在不同的程序员看来,有见然不同的见解。c程序员见了这些必然两眼放光,但会对其性能,尤其是处理数据的thoughtput或latency存疑;erlang程序员估计会喜欢大部分特性,但对messaging和concurrency不屑一顾;而python程序员兴奋之余,却又会产生「要是python能够产生一个脱离运行时的可执行文件,且没有GIL作为制肘,那golang还有市场空间么」这样一厢情愿的白日梦。

所以语言特性,并不能成为我为golang叫好的原因。在我看来,golang为软件开发注入了新的思想和活力,让很多曾经的「想当然」有了新的思路,这才是最重要的。

试举一二。

首先,concurrency在别的语言那里被当做parallelism的一种手段,在golang这里却变成了解耦系统,降低复杂度的神器。如果你没看过Rob Pike的concurrency is not parallelism [5],强烈建议看一下,即使你并不是golang的拥趸。也许是诞生于unix发明人之手(Ken Thompson),golang里面处处可见unix精髓。​​chan/go​​ 这对欢喜冤家,就像跳动的精灵一样,将函数之间亘古不变的调用关系变成了一对对,不,一群群pipe。最容易让人拿来说事的是 ​​io.Copy()​​:

func Chat(a, b io.ReadWriteCloser) {     err_chan := make(chan error, 1)      go cp(a, b, err_chan)     go cp(b, a, err_chan)      if err := <- err_chan; err != nil {         log.Println(err)     }      a.Close()     b.Close() }  func cp(w io.Writer, r io.Reader, err_chan chan<- error) {     _, err := io.Copy(w, r)     err_chan <- err }

你无法想象比这再优美的代码:它在a/b两个流之间进行全双工异步通讯,当发生错误时告知 ​​Chat()​​ 所在的协程,记录错误并优雅退出。对于golang来说,把 ​​chan/go​​ 当成解耦的工具,运用得当的话,代码就有了魔力。

其次,golang的某些哲学值得其它语言的使用者思考。golang的语言设计中透着妥协,它期望把concurrency model带至主流。所以它选择了C-like的语法,走imperative language的路,而不是把自己打造成functional language,像Haskell那样,也因此,并发模型上,golang选择了CSP model [6],因为CSP对imperative language的支持很友好,不必使用一些奇怪的语法和新颖的概念去表达。相比之下,大多functional language都选择了Actor model [7],如erlang,scala。所以在golang里你见到的是channel,erlang里你见到的是process(actor);golang里的goroutine是匿名的,erlang里的process是有pid的;golang里消息的发送是同步的,阻塞的 [8],erlang里消息是异步的,非阻塞的,等等等等。

做为一个concurrent language,golang最受诟病的是指针竟然也能够在channel中传递。要知道,concurrency的精髓在于don’t share state。erlang之父Joe Armstrong在其 "Programming Erlang" 中写到:


We don’t have shared memory. I have my memory. You have yours. We have two brains, one each. They are not joined together. To change your memory, I send you a message: I talk, or I wave my arms.


(题外话:这是本好书,里面有不少思想,即使不用erlang,也可以读读)

所以对于golang的concurrency支持,erlang的使用者,或者有functional programming情节的人,绝对反感。但golang的设计者有其道理:为了让c/java/python等imperative language的使用者能更好地接纳golang;也有其哲学:


Don’t communicate by sharing memory, share memory by communicating.


再次,​golang的错误处理另辟蹊跷,在几乎所有主流语言都支持 try/catch 结构的世界里,「反人类」地使用了类似于c的通过返回值返回错误的「原始」方式​。是的,错误处理还可以有不一样的玩法。这种方式简单清晰,通过有效应用 ​​chan​​(如上例),错误还可以像其它支持 ​​try/catch​​ 的语言那样bubble up。我不是语言设计的专家,姑且很不专业地估计一下为什么golang这么设计。对于一门以concurrency见长地语言,concurrency必定无处不在的。如果采用异常模式,那问题来了:当一个goroutine有未处理的错误,谁来处理这个错误?自然是主线程,这是其它编程语言的做法。然而,主线程是否是最佳的选择?不一定。如果使用 ​​try/catch​​ 结构,没得选择,但golang通过使用返回值来返回错误,让开发者可以选择把错误交给哪个goroutine处理。这可以催生一些有意思的模式,比如说有个goroutine专门负责过滤错误,只有极其严重的才bubble up到主线程,让其清理资源并退出程序。当然,这种方式与concurrency放在一起也还是会有一些缺陷,所以golang需要 ​​defer​​, ​​panic​​ 和 ​​recover​​ 来帮忙。最后,golang把code by convention玩到了极致。代码的行为靠约定来完成,大写字母开头的函数自动export,implicit interface等等,让其它语言中需要额外代码描述的事情在这里不必赘述,如果不是golang,恐怕很多人都无法想象继承可以这么玩。我倒不是说这么做就是最佳实践,但它是继 ​​io​​ [9] 和 ​​javascript​​ 通过原型继承让我大开眼界后,又一个让我感慨一下,思考一下的实践。

嗯,就写这些吧。

衷心希望golang在下一个五年能够大放光彩,也希望那些和程序君一样的还在和C语言一起奋斗的一部分可怜人 [10] 可以早日转战在golang的竞技场。神马,程序君为什么不能用golang写代码呢?唉,尼玛golang不厚道,支持freebsd的最低版本是8,谁能帮我在6.x/7.x上编过,务必告知,给跪了。

disclainer: 程序君golang经验不足一年,除去hello world系列代码,代码数量不足5千行,才疏学浅,本文如有错漏,敬请指教。



欢迎订阅公众号『程序人生』