我为什么放弃Go语言?

有好几次,当我想起来的时候,总是会问自己:我为什么要放弃Go语言?这个决定是正确的吗?是明智和理性的吗?事实上我一直在认真思考这个问题。

开门见山地说,我当初放弃Go语言(golang),就是由于两个“不爽”:第一,对Go语言本身不爽;第二,对Go语言社区里的某些人不爽。毫无疑问,这是很主观的结论。可是我有足够详实的客观的论据,支撑这个看似主观的结论。

第0节:我的Go语言经历

先说说我的经历吧,以避免被无缘无故地当作Go语言的低级黑。

2009年底,Go语言(golang)第一个公开版本号公布,笼罩着“Google公司制造”的光环,吸引了很多慕名而来的尝鲜者,我(Liigo)也身居当中,笼统的看了一些Go语言的资料,学习了基础的教程,因对其语法中的分号和花括号不满,非常快就遗忘掉了,没拿它当一回事。

两年之后,2011年底,Go语言公布1.0的计划被提上日程,相关的报道又多起来,我再次关注它,[又一次评估][1]之后决定深入參与Go语言。我订阅了其users、nuts、dev、commits等官方邮件组,坚持每天阅读当中的电子邮件,以及开发人员提交的每一次源码更新,给Go提交了很多改进意见,甚至包含[改动Go语言编译器源码][2]直接參与开发任务。如此持续了数月时间。

到2012年初,Go 1.0公布,语言和标准库都已经基本定型,不可能再有大幅改进,我对Go语言未能在1.0定型之前更上一个台阶、实现自我突破,甚至带着诸多明显缺陷走向1.0,感到非常失望,因而逐渐疏远了它(所以Go 1.0之后的事情我非常少关心)。后来看到即将公布的Go 1.1的Release Note,发现语言层面没有太大改变,仅仅是在库和工具层面有所修补和改进,感到它尚在幼年就失去成长的动力,越发失望。外加Go语言社区里的某些人,当中也包含Google公司负责开发Go语言的某些人,其态度、言行,让我极度厌恶,促使我决绝地离弃Go语言。

第1节:我为什么对Go语言不爽?

Go语言有非常多让我不爽之处,这里列出我如今还能记起的当中一部分,排名基本上不分先后。读者们耐心地看完之后,还能淡定地说一句“我不在乎”吗?

1.1 不同意左花括号另起一行

关于对花括号的摆放,在C语言、C++、Java、C#等社区中,十余年来存在持续争议,从未形成一致意见。在我看来,这本来就是主观倾向非常重的抉择,不违反原则不涉及是非的情况下,不应该搞一刀切,让程序猿或团队自己选择就足够了。编程语言本身强行限制,把自己的喜好强加给别人,得不偿失。不管倾向于当中随意一种,必定得罪与其对立的一群人。尽管我如今已经习惯了把左花括号放在行尾,但一想到被禁止其它选择,就感到十分不爽。Go语言这这个问题上,没有做到“团结一切能够团结的力量”不说,还有意给自己树敌,太失败了。

1.2 编译器莫名其妙地给行尾加上分号

对Go语言本身而言,行尾的分号是能够省略的。可是在其编译器(gc)的实现中,为了方便编译器开发人员,却在词法分析阶段强行加入了行尾的分号,反过来又影响到语言规范,对“如何加入分号”做出特殊规定。这样的变态做法前无古人。在左花括号被意外放到下一行行首的情况下,它自己主动在上一行行尾加入的分号,会导致莫名其妙的编译错误(Go 1.0之前),连它自己都解释不明确。假设实在处理不好分号,干脆不要省略分号得了;或者,Scala和JavaScript的编译器是开源的,跟它们学学怎么处理省略行尾分号能够吗?

1.3 极度强调编译速度,不惜放弃本应提供的功能

程序猿是人不是神,编码过程中免不了由于大意或疏忽犯一些错。当中有一些,是大家集体性的非常easy就中招的错误(Go语言里的样例我临时想不起来,C++里的样例有“基类析构函数不是虚函数”)。这时候编译器应该站出来,多做一些检查、约束、核对性工作,尽量阻止常规错误的发生,尽量不让有潜在错误的代码编译通过,必要时给出一些警告或提示,让程序猿留意。编译器不就是机器么,不就是应该多做脏活累活杂活、降低人的心智负担么?编译器多做一项检查,可能会避免数十万程序猿今后多年内无数次犯相同的错误,节省的时间不计其数,这是功德无量的好事。可是Go编译器的作者们可不这么想,他们不愿意自己多花几个小时给编译器添加新功能,认为那是亏本,反而减慢了编译速度。他们以影响编译速度为由,拒绝了非常多对编译器改进的要求。典型的因噎废食。强调编译速度固然值得观赏,但假设因此放弃应有的功能,我不赞成。

1.4 错误处理机制太原始

在Go语言中处理错误的基本模式是:函数通常返回多个值,当中最后一个值是error类型,用于表示错误类型极其描写叙述;调用者每次调用完一个函数,都须要检查这个error并进行对应的错误处理。这样的模式跟C语言那种非常原始的错误处理相比方出一辙,并无实质性改进。实际应用中非常easy形成多层嵌套的if else语句,能够想一想这个编码场景:先推断文件是否存在,假设存在则打开文件,假设打开成功则读取文件,假设读取成功再写入一段数据,最后关闭文件,别忘了还要处理每一步骤中出现错误的情况,这代码写出来得有多变态、多丑陋?实践中普遍的做法是,推断操作出错后提前return,以避免多层花括号嵌套,但这么做的后果是,很多错误处理代码被放在前面突出的位置,常规的处理逻辑反而被掩埋到后面去了。并且,error对象的标准接口仅仅能返回一个错误文本,有时候调用者为了区分不同的错误类型,甚至须要解析该文本。除此之外,你仅仅能手工强制转换error类型到特定子类型。至于panic - recover机制,致命的缺陷是不能跨越库的边界使用,注定是一个半成品,最多仅仅能在自己的pkg里面玩一玩。Java的异常处理尽管也有自身的问题(比方Checked Exceptions),但整体上还是比Go的错误处理高明非常多。

1.5 垃圾回收器(GC)不完好、有重大缺陷

在Go 1.0前夕,其垃圾回收器在32位环境下有内存泄漏,一直拖着不肯改进,这且不说。Go语言垃圾回收器真正致命的缺陷是,会导致整个进程不可预知的间歇性停顿。像某些大型后台服务程序,如游戏server、APP容器等,因为占用内存巨大,其内存对象数量极多,GC完毕一次回收周期,可能须要数秒甚至更长时间,这段时间内,整个服务进程是堵塞的、停顿的,在外界看来就是服务中断、无响应,再牛逼的并发机制到了这里统统失效。垃圾回收器定期启动,每次启动就导致短暂的服务中断,这样下去,还有人敢用吗?这但是后台server进程,是Go语言的重点应用领域。以上现象可不是我如果出来的,而是事实存在的现实问题,受其严重困扰的也不是一家两家了(截止到2014年初)。在实践中,你必须努力降低进程中的对象数量,以便把GC导致的间歇性停顿控制在可接受范围内。除此之外你别无选择(难道你还想自己更换GC算法、甚至砍掉GC?那还是Go语言吗?)。跳出圈外,我最近一直在思考,一定须要垃圾回收器吗?没有垃圾回收器就一定是历史的倒退吗?(可能会新写一篇博客文章专题探讨。)

1.6 禁止未使用变量和多余import

Go编译器不同意存在被未被使用的变量和多余的import,假设存在,必定导致编译错误。可是现实情况是,在代码编写、重构、调试过程中,比如,暂时性的凝视掉一行代码,非常easy就会导致同一时候出现未使用的变量和多余的import,直接编译错误了,你必须对应的把变量定义凝视掉,再翻页回到文件首部把多余的import也凝视掉,……等事情办完了,想把刚才凝视的代码找回来,又要好几个麻烦的步骤。另一个让人蛋疼的问题,编写数据库相关的代码时,假设你import某数据库驱动的pkg,它编译给你报错,说不须要import这个未被使用的pkg;但假设你听信编译器的话删掉该import,编译是通过了,执行时必定报错,说找不到数据库驱动;你看看程序猿被折腾的两边不是人,最后不得不请出大神_。对待这样的问题,一个比較好的解决方式是,视其为编译警告而非编译错误。可是Go语言开发人员非常固执,不容许这样的折中方案。

1.7 创建对象的方式太多令人纠结

创建对象的方式,调用new函数、调用make函数、调用New方法、使用花括号语法直接初始化结构体,你选哪一种?不好选择,由于没有一个固定的模式。从实践中看,假设要创建一个语言内置类型(如channel、map)的对象,通经常使用make函数创建;假设要创建标准库或第三方库定义的类型的对象,首先要去文档里找一下有没有New方法,假设有就最好调用New方法创建对象,假设没有New方法,则退而求其次,用初始化结构体的方式创建其对象。这个过程颇为周折,不像C++、Java、C#那样直接new即可了。

1.8 对象没有构造函数和析构函数

没有构造函数还好说,毕竟还有自己定义的New方法,大致也算是构造函数了。没有析构函数就比較难受了,没法实现RAII。额外的人工处理资源清理工作,无疑加重了程序猿的心智负担。没人性啊,还嫌我们程序猿加班还少吗?C++里有析构函数,Java里尽管没有析构函数但是有人家finally语句啊,Go呢,什么都没有。没错,你有个defer,但是那个defer问题更大,详见下文吧。

1.9 defer语句的语义设定不甚合理

Go语言设计defer语句的出发点是好的,把释放资源的“代码”放在靠近创建资源的地方,但把释放资源的“动作”推迟(defer)到函数返回前运行。遗憾的是其运行时机的设置似乎有些不甚合理。设想有一个须要长期运行的函数,当中有无限循环语句,在循环体内不断的创建资源(或分配内存),并用defer语句确保释放。由于函数一直运行没有返回,全部defer语句都得不到运行,循环过程中创建的大量短暂性资源一直积累着,得不到回收。并且,系统为了存储defer列表还要额外占用资源,也是持续添加的。这样下去,过不了多久,整个系统就要由于资源耗尽而崩溃。像这类长期运行的函数,http.ListenAndServe()就是典型的样例。在Go语言重点应用领域,能够说差点儿每个后台服务程序都必定有这么一类函数,往往还都是程序的核心部分。假设程序猿不小心在这些函数中使用了defer语句,能够说后患无穷。假设语言设计者把defer的语义设定为在所属代码块结束时(而非函数返回时)运行,是不是更好一点呢?但是Go 1.0早已公布定型,为了保持向后兼容性,已经不可能改变了。小心使用defer语句!一不小心就中招。

1.10 很多语言内置设施不支持用户定义的类型

for in、make、range、channel、map等都仅支持语言内置类型,不支持用户定义的类型(?)。用户定义的类型没法支持for in循环,用户不能编写像make、range那样“參数类型和个数”甚至“返回值类型和个数”都可变的函数,不能编写像channel、map那样类似泛型的数据类型。语言内置的那些东西,处处充斥着斧凿的痕迹。这体现了语言设计的局限性、封闭性、不完好性,像是新手作品——且不论其设计者和实现者怎样权威。

1.11 没有泛型支持,常见数据类型接口丑陋

没有泛型的话,List、Set、Tree这些常见的基础性数据类型的接口就仅仅能非常丑陋:放进去的对象是一个详细的类型,取出来之后成了无类型的interface{}(能够视为全部类型的基础类型),还得强制类型转换之后才干继续使用,令人无语。Go语言缺少min、max这类函数,求数值绝对值的函数abs仅仅接收/返回双精度小数类型,排序接口仅仅能借助sort.Interface无奈的回避了被比較对象的类型,等等等等,都是没有泛型导致的结果。没有泛型,接口非常难优雅起来。Go开发人员没有明白拒绝泛型,仅仅是说还没有找到非常好的方法实现泛型(能不能学学已经开源的语言呀)。现实是,Go 1.0已经定型,泛型还没有,那些丑陋的接口为了保持向后兼容必须长期存在着。

1.12 实现接口不须要明白声明

这一条一般是被当作Go语言的优点来宣传的。可是也有人不赞同,比方我。假设一个类型用Go语言的方式默默的实现了某个接口,使用者和代码维护者都非常难发现这一点(除非细致核对该类型的每个方法的函数签名,并跟全部可能的接口定义相互对比),自然也想不到与该接口有关的应用,显得十分隐晦,不直观。支持者可能会辩讲解,我能够在文档中注明它实现了哪些接口。问题是,写在文档中,还不如直接写到类型定义上呢,至少还能得到编译器的静态类型检查。缺少了编译器的支持,当接口类型的函数签名被改变时,当实现该接口的类型方法被无意中改变时,实现者可能非常难意识到,该类型实现该接口的隐含约束其实已经被打破了。又有人辩讲解,我能够通过单元測试确保类型正确实现了接口呀。我想说的是,明明能够通过明白声明实现接口,享受编译器提供的类型检查,你却要自己找麻烦,去写原本多余的单元測试,找虐非常爽吗?Go语言的这样的做法,除了降低一些对接口所在库的依赖之外,没有其它优点,得不偿失。

1.13 省掉小括号却省不掉花括号

Go语言里面的if语句,其条件表达式不须要用小括号扩起来,这被作为“代码比較简洁”的证据来宣传。但是,你省掉了小括号,却不能省掉大括号啊,一条完整的if语句至少还得三行吧,人家C、C++、Java都能够在一行之内搞定的(能够省掉花括号)。人家还有x?a:b表达式呢,也是一行搞定,你Go语言用if else写至少得五行吧?哪里简洁了?

1.14 编译生成的可运行文件尺寸很大

记得当年我写了一个非常easy的程序,把全部系统环境变量的名称和值输出到控制台,核心代码也就那么三五行,结果编译出来把我吓坏了:EXE文件的大小超过4MB。假设是C语言写的相同功能的程序,0.04MB都是多的。我把这个信息反馈到官方社区,结果人家不在乎。是,我知道如今的硬盘容量都数百GB、上TB了……可您这样的优化程度……怎么让我相信您在其它地方也能做到不错呢。(再次强调一遍,我全部的经验和数据都来自Go 1.0公布前夕。)

1.15 不支持动态载入类库

静态编译的程序当然是非常好的,没有额外的执行时依赖,部署时非常方便。可是之前我们说了,静态编译的文件尺寸非常大。假设一个软件系统由多个可执行程序构成,累加起来就非常可观。假设用动态编译,公布时带同一套动态库,能够节省非常多容量。更关键的是,动态库能够执行时载入和卸载,这是静态库做不到的。还有那些LGPL等协议的第三方C库受版权限制是不同意静态编译的。至于动态库的版本号管理难题,能够通过给动态库内的全部符号加入版本号号解决。不管怎样,应该给予程序猿选择权,让他们自己决定使用静态库还是动态库。一刀切的拒绝动态编译是不合适的。

1.16 其它

  • 不支持方法和函数重载(overload)
  • 导入pkg的import语句后边部分居然是文本(import ”fmt”)
  • 没有enum类型,全局性常量难以分类,iota把简单的事情复杂化
  • 定义对象方法时,receiver类型应该选用指针还是非指针让人纠结
  • 定义结构体和接口的语法稍繁,interface XXX{}struct YYY{} 不是更简洁吗?前面加上typekeyword显得罗嗦。
  • 測试类库testing里面没有AssertEqual函数,标准库的单元測试代码中充斥着if a != b { t.Fatal(...) }
  • 语言太简单,以至于不得不放弃非常多实用的特性,“保持语言简单”往往成为拒绝改进的理由。
  • 标准库的实现整体来说不甚理想,其代码质量大概处于“基本可用”的程度,真正到企业级应用领域,往往就会暴露出诸多不足之处。
  • 版本号都发展到1.2了,goroutine调度器依然默认仅使用一个系统线程。GOMAXPROCS的长期存在似乎暗示着官方从来没有足够的信心,让调度器安全正确的执行在多核环境中。这跟Go语言自身以并发为核心的定位有致命的矛盾。

上面列出的是我眼下还能想到的对Go语言的不爽之处,毕竟时间过去两年多,另一些早就遗忘了。当中一部分固然是小不爽,可能忍一忍就过去了,可是非常多不爽积累起来,总会时不时地让人难受,时间久了有自虐的感觉。程序猿的工作生活本来就够枯燥的,何必呢。

必需要说的是,对于当中大多数不爽之处,我(Liigo)都以前试图改变过它们:在Go 1.0版本号公布之前,我在其官方邮件组提过非常多意见和建议,极力据理力争,能够说付出非常大努力,目的就是希望定型后的Go语言是一个相对完好的、没有明显缺陷的编程语言。结果是令人失望的,我人微言轻、势单力薄,不可能影响整个语言的发展走向。1.0之前,最佳的否定自我、超越自我的机会,就这么遗憾地错过了。我终于发现,非常多时候不是技术问题,而是技术人员的问题。

第2节:我为什么对Go语言的某些人不爽?

这里提到的“某些人”主要是两类:一、负责专职开发Go语言的Google公司员工;二、Go语言的推崇者和脑残粉丝。我跟这两类人打过非常多交道,不胜其烦。再次强调一遍,我指的是“某些”人,而不是全部人,请不要对号入座。

Google公司内部负责专职开发Go语言的核心开发组某些成员,他们倾向于闭门造车,固执己见,对第三方提出的建议不重视。他们经常挂在嘴边的口头禅是:现有的做法非常好、不须要那个功能、我们开发Go语言是给Google自己用的、Google不须要那个功能、假设你一定要改请fork之后自己改、别干提意见请提交代码。非常多言行都是“反开源”的。通过一些详细的样例,还能更形象的看清这一层。就留下作为课后作业吧。

我最不能接受的就是他们对1.0版本号的散漫处理。那时候Go还没到1.0,初出茅庐的小学生,有非常大的改进空间,是全面翻新的最佳时机,彼时不改更待何时?1.0是打地基的版本号,基础不牢靠,等1.0定型之后,处处受到向后兼容性的牵制,束手缚脚,每前进一步都阻力重重。急于公布1.0,过早定型,留下诸多遗憾,彰显了开发人员的功利性强,在技术上不追求尽善尽美。

Go语言的核心开发成员,他们日常的开发工作是使用C语言——Go语言的编译器和执行时库,包含语言核心数据结构map、channel、scheduler,都是C开发的——真正用自己开发的Go语言进行实际的大型应用开发的机会并不多,尽管标准库是用Go语言自己写的,但他们却没有大范围使用标准库的经历。实际上,他们缺少使用Go语言的实战开发经验,往往不知道处于开发第一线的用户真正须要什么,无法做到设身处地为程序猿着想。缺少使用Go语言的亲身经历,也意味着他们不能在日常开发中,及时发现和改进Go语言的不足。这也是他们往往自我感觉良好的原因。

Go语言社区里,有一大批Go语言的推崇者和脑残粉丝,他们满足于现状,不思进取,处处维护心中的“神”,容不得批评意见,不支持对语言的改进要求。当年我对Go语言的非常多批评和改进意见,极少得到他们的支持,他们不但不支持还给予打击,我就纳闷了,他们难道不希望Go语言更完好、更优秀吗?我后来才意识到,他们跟乔帮主的苹果脑残粉丝们,言行一脉相承,具有极端宗教倾向,神化主子、打击异己真是不遗余力呀。简简单单的技术问题,就能被他们上升到意识形态之争。现实的样例是蛮多的,有兴趣的到网上去找吧。正是由于他们的存在,导致很多其它理智、清醒的Go语言用户无法真正融入整个社区。

假设一个项目、团队、社区,到处充斥着赞美、孤芳自赏、自我满足、不思进取,排斥不允许见,拒绝接纳新方案,我想不到它还有什么前进的动力。逆水行舟,是不进反退的。

第3节:还有比Go语言更好的选择吗?

我始终坚持一个颇有辩证法意味的哲学观点:在更好的替代品出现之前,现有的就是最好的。失望是没实用的,抱怨是没实用的,要么接受,要么逃离。我以前努力尝试过接受Go语言,失败之后,注定要逃离。发现更好的替代品之后,无疑加速了逃离过程。还有比Go语言更好的替代品吗?当然有。作为一个屌丝程序猿,我应该告诉你它是什么,可是我不说。如今还不是时候。我如今不想把这两门编程语言对立起来,引发还有一场潜在的语言战争。这不是此文的本意。假设你非要从现有信息中猜測它是什么,那全然是你自己的事。假设你原意等,它也许非常快会浮出水面,也未可知。

第4节:写在最后

我不原意被别人代表,也不愿意代表别人。这篇文章写的是我,一个叫Liigo的80后屌丝程序猿,自己的观点。你全然能够主观地觉得它是主观的,也全然能够客观地以为它是客观的,不管怎样,那是你的观点。

这篇文字是从记忆里收拾出来的。有些细节虽可考,而不值得考。——我早已逃离,不愿再回到当年的场景。文中涉及的某些细节,可能会由于些许偏差,影响其准确性;也可能会由于缺少出处,影响其客观性。假设有人较真,非要去核实,我相信那些东西应该还在那里。

Go语言也非上文所述一无是处,它当然有它的优势和特色。读者们推断一件事物,应该是优劣并陈,做综合分析,不能单听我一家负面之言。可是它的那些不爽之处,始终让我不爽,且不能从其优秀处得以全然中和,这是我不得不放弃它的原因。