自Google在2009年发布Go语言的第一个正式版之后,这门语言就以出色的语言特性受到大家的追捧,尤其是在需要高并发的场景下,大家都会想到是不是该用Go。随后,在国内涌现出了一批以七牛为代表的使用Go作为主要语言的团队,而许式伟大神本人也在各种场合下极力推动Go在国内的发展,于是在这种大环境下,中国的Go开发者群体逐渐超越了其他地区。
那么问题来了,业余时间好学是一回事,真正要将一个新东西运用到生产中则是另一回事。JavaScript的开发者可以义无反顾地选择Node.js,但是对于Java开发者来说,在下一个大项目里究竟是该选择Go,还是Java呢?
语言本身
首先,需要说明一下,作为一个技术决策者,在进行技术选型时并不能单方面地根据语言本身的特点直接下结论。实际情况下,大多数人会使用一系列的框架、库及工具,简而言之就是会考虑很多周边生态环境的因素,同时还要结合公司的特点、各种历史问题和实际客观因素等等一系列的考虑点综合下来才能完成决策。所以,接下来我们先从语言开始,一步一步来分析下在你的项目中选择Go是否合适。
Go在高并发编程方面无疑是出众的,通过goroutine从语言层面支持了协程,这是Java等语言所无法比拟的,这也是大多数人在面对高并发场景选择Go的重要原因之一。虽然Java有Kilim之类的框架,但没有语言层的支持始终稍逊一筹。
除此之外,Go的其他语法也很有趣,比如多返回值,在一定程度上为开发者带来了一定的便利性。试想,为了返回两到三个值,不得不封装一个对象,或者抹去业务名称使用Map
、List
等集合类,高级一点用Apache的Pair
和Triple
,虽然可行,但始终不如Go的实现来得优雅。在此之上,Go也统一了异常的返回方式,不用再去纠结是通过抛异常还是错误码来判断是否成功,多返回值的最后一个是Error
就行了。
Go在语言的原生类型中支持了常用的一些结构,比如map
和slice
,而其他语言中它们更多是存在于库中,这也体现了这门语言是从实践角度出发的特点,既然人人都需要,为什么不在语言层面支持它呢。函数作为一等公民出现在了Go语言里,不过Java在最近的Java 8中也有了Lambda表达式,也算是有进步了。
其他的一些特性,则属于锦上添花型的,比如不定参数,早在2004年的Java 1.5中就对varargs有支持了;多重赋值在Ruby中也有出现,但除了多返回值赋值,以及让你在变量交换值时少写一个中间变量,让代码更美观一些之外,其他的作用着实不是怎么明显。
说了这么多Go的优点,当然它也有一些问题,比如GC,说到它,Java不得不露出洁白的牙齿,虽然在大堆GC上G1还有些不尽如人意,但Java的GC已经发展了很多年,各种策略也比较成熟,CMS或G1足以应付大多数场景,实在有要求还能用Azul Zing JVM。不过从最新的Go 1.5的消息来看,Go的GC实现有了很大地提升,顺便一提的是GOMAXPROCS
默认也从1变成了CPU核数,看来官方对Go在多核的利用方面更有信心了。
许世伟在《Go 语言编程》的前言中预言未来10年,Go会取代Java,位居编程榜之首,当时是2012年,为了看看2009年TIOBE年度编程语言如今的排名,笔者在撰写本文时特意去TIOBE看了下,最近的2015年8月排行榜,Java以19.274%位居榜首,Go已经跌出了前50,这不禁让人有些意外。
但总体上来说,笔者认为Go在语言层面的表现还是相当出色的,解决了一些编程中的痛点,学习曲线也能够接受,特别是对于那些有C/C++背景的人,会感觉十分亲切。
工程问题
一个人写代码时可以很随性,想怎么写就怎么写,但当一个人变成一个团队后,这种随性或者说随便就会带来很多问题,于是就诞生了编码规范这玩意儿,大厂基本都有自己的编码规范,比如Google就有针对不下十种编程语言的规范。团队内约定一套编码规范能够很大程度上地确保代码的风格,降低阅读沟通的成本。Go内置了一套编码规范,违反了该规范代码就无法编译通过,可以说只要你是写Go的,那你的代码就不会太难看,当然Go也没有把所有东西就强制死,还有一些推荐的规范可以通过gofmt
进行格式化,但这步不是必须的。
虽然Go自己解决了这个问题,但并不能说Java在这方面是空白,Java发展至今周边工具无数,并不缺成熟的代码静态分析工具,比如CheckStyle、PMD和FindBugs,它们不仅能扫描编码规范的问题,甚至还能扫描代码中潜在的问题并给出解决方案,并且使用方便,在Java开发者社区中有很高地接受度,应该说大多数靠谱地开发者都会使用这些工具。除此之外,一些大厂也有自己的强制手段,比如百度内部也有很多语言的编码规范,而且大部分情况下如果没有通过编码规范的扫描,你是无法提交代码的;还有一些公司会在持续集成过程中加入代码扫描,有FindBugs高优先级的问题时必须修复才能进入下一个阶段。所以说Go在这个问题上的优势并不明显,或者说在一个成熟的环境下,这只是合格而已。
这里需要强调笔者的一个观点:
Go在语言本身和发行包中融入了很多最佳实践,正是这些前人的经验才让它看起来如此优秀。拿这么个海陆空混编特种部队去和Java、C、Ruby这些语言本身做对比,显得不太公平,所以本文在考虑问题时都会结合语言及其生态圈中的成员,毕竟这才更接近真实的情况。
Go本身对项目结构有一套约定,代码放哪里,测试文件如何命名,编译打包后的结果输出到哪个目录,甚至还有go cover
这种统计测试覆盖率的命令行,开发者不用在这些问题上太过纠结,再一次体现了Go注重工程实践的特点。回过头来,Java方面,Maven
、Gradle
都是注重于工程生命周期管理的工具,而且Maven
更是历史悠久,被广泛用于各种项目之中。以Maven
为例,不仅能够实现上述所有功能,还有很强的插件扩展能力,这里需要的只是一次性维护好pom.xml
文件就行了,由于Maven
的使用群很大,网上有大量的范例,甚至还有很多生成工程的工具和模板,所以使用成本并不高。
这里还要衍生出一个话题,就是依赖管理,在开发代码时,势必需要依赖很多外部的东西,Go可以直接import
远程的内容,这个特性很有创意,但并不能很好地解决版本的问题,在Maven
或Gradle
里,我们可以直接指定各个依赖项甚至是插件的版本,工具会自动从仓库中下载它们。如果需要同时在同一个系统的不同模块里依赖同一个库的不同版本,我们还能够通过OSGi这种略显复杂的手段来实现,在模块化方面,Jagsaw
虽然被一延再延,但估计有望纳入Java 9,这个特性也会解决不少问题。而根据Golang实践群中大家的讨论,似乎godep、gb和gvt都不尽如人意,在这点上看来Go还有一段路要走。
综上所述,Go在工程方面的确有不少亮点,吸纳了很多最佳实践,甚至可以说用Go之后更容易写出规范的代码,有好的项目结构,但与生态圈完备的Java相比,Go并不占优势,因为最终代码的质量还是由人决定的,双方都不缺好的工具,所以这方面的特点并不能影响技术选型的决策。
开发实践
Talk is cheap. Show me the code.
下面进入编码环节,先从Go引以为傲的并发开始,《Go语言编程》的前言中有这样一段代码:
func run(arg string) { // ... } func main() { go run("test") ... }
书中与之对比的Java代码有12行,而且还是线程,不是协程,对比很明显,但那是在2012年的时候,时至今日,Java已经发展到了Java 8,3年了,看看如今的Java代码会是什么样的:
public class ThreadDemo { public static void main(String[] args) { String str = "test"; // 为了和原先的Java版本对照,说明能传参进入线程内,在外声明了一个字符串,其实可以直接写在Lambda里 new Thread(() -> { /* do sth. with str */ }).start(); } }
不是协程仍是硬伤,但有了Lambda表达式,代码短了不少。不过话又说回来,这样的比较并没有太多意义,所以各位Go粉也不用站出来说Go也支持闭包,Go的版本也能精简。我们比的不是谁写的短,在Java实践中,大多数时候大家会选择线程池,而不是自己new
一个Thread
对象,Doug Lea大神的Java并发包非常的好用,而且很靠谱。另外,并发中处理的内容才是关键,新启一个线程或者协程才是万里长城的第一步,如果其中的业务逻辑有10个分支,还要多次访问数据库并调用远程服务,那无论用什么语言都白搭。所以在业务逻辑复杂的情况下,语言的差异并不会太明显,至少在Java和Go的对比下不明显,至于其他更高阶、表达力更强的语言(比如Common Lisp),大家就要拼智商了。
还有一些情况中,由于客观因素制约,完全就无法使用Go,比如现在如火如荼的互联网金融系统里,与银行对接的系统几乎没有选择,都是Java实现的,因为有的银行只会给Jar包啊……给Jar包啊……Jar包啊……如果是个so文件,也许还能用cgo应付一下,面对一个Jar你让Go该何去何从?
抛开这些让人心烦的问题,让我们再来看看现在比较常见的如何实现REST服务。说到这里,就一定要祭出国人出品的Beego框架。一个最简单的REST服务可以是这样的:
package main import ( "github.com/astaxie/beego" ) type MainController struct { beego.Controller } func (this *MainController) Get() { this.Ctx.WriteString("hello world!") } func main() { beego.Router("/", &MainController{}) beego.Run() }
既然Go方面,我们使用了一套框架,那么Java方面,我们一样也选择一个成熟的框架,Spring在Java EE方面基本可以算是事实标准,而Spring Boot更是大大提升了Spring项目的开发效率,看看同样实现一个REST服务,在SpringBoot里是怎么做的。
首先,到start.spring.io根据需要生成项目骨架(其实完全可以方便地自己通过Maven手工配置依赖或者是用CLI工具来创建),为了后续的演示,这里我会选上“Web”、“Actuator”和“Remote Shell”,其实就是多了两个Maven的依赖,下文运维部分会提到,然后随便找个顺手的IDE打开工程,敲入如下代码就行了(import、包和类定义的部分基本都是IDE生成的)。
package demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class DemoApplication { @RequestMapping("/") public String sayHello() { return "hello world!"; } public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
运行这段代码会自动启动内置Tomcat容器,访问http://localhost:8080/就能看到输出了。因为其实就是Spring,所以可以毫无压力地与其他各种框架设施组合,也没有太多学习成本。
可见两者在实现REST服务方面,并没有太大的差别,加之上文提到的业务逻辑问题,只要运用恰当的工具,两种语言之间并不会产生质的差异。
Beego中的ORM支持MySQL、PostgreSQL和Sqlite3,而在Java里Hibernate和myBatis这样的ORM工具几乎能通吃大多数常见的关系型数据库,且相当成熟,社区配备了各种自动生成工具来简化使用,行业里还有JPA这样的公认标准。纵观Go的ORM工具,大家还是在探讨,究竟哪个才好用呢?切到NoSQL方面,双方都有大量的驱动可以使用,比如MongoDB和Redis都有详尽的驱动列表,MongoDB还没有官方驱动,但有社区维护的mgo,算是打成平手吧。再大一点,像用到Hadoop、Spark和Storm的场景下,似乎Java的出镜率更高,或者是直接通过Streaming方式就解决了,此处也就不再展开了。
虽然说了这么多问题,但如果真的遇到了大流量、高并发的场景,需要从头开始开发用来处理这些问题的基础设施时,Go还是不错的选择。比如,七牛这样的云服务提供商,又或者是BFE(Baidu Front End,号称可能是全世界流量最大的Go语言集群 ,在2015年的Velocity大会上留下了它的身影——图1和图2)这样的硬货,请不要纠结。
运维
写完代码只是万里长征的一小步,后面还有一大堆的事情等着你去解决,比如怎么把写完的代码编译、打包、发布上线。编译打包就不说了,Go的命令行工具go build
就能直接把你的代码连同它的所有依赖一起打成一个可执行文件。至于部署,大家都称赞Go的部署没有依赖(除了对glibc的版本有要求,不考虑需要cgo的情况),直接把可执行文件往那里一扔就好了,非常方便。Go内置了强大的HTTP支持,不需要其他Web服务器来做支撑就能获得不错的性能。
再来看看Java,按照常理,一般都会使用Maven
或者Gradle
来处理编译、打包,甚至是发布,仍旧以Maven
为例,mvn package
就能完成编译和打包。可以选择Jar包,如果是Web项目部署到容器里的话可以是War包,也可以将各种资源打包到一起放到压缩包(zip、tar等等)里,这个步骤并不复杂。
接下来的部署环节,大家就有话要说了,“Write Once, Run Anywhere”这曾是Java的宣传语,但正是这句话一直被大家诟病,其实如果代码中不使用平台特定的内容(比如避免绑定在WebLogic上),不使用某个特定版本JDK的内部类(比如com.sun
里的东西,这种做法本来就不推荐),Java的代码还是能够做到编译后在任何地方都能运行的,事实上现在绝大部分情况下,大家也都是这么做的,看看广大的Java库都是发布Jar到Maven仓库的,也没谁让你直接拉源码来编译。在不同的环境下,只需要部署了对应的JDK就好了(一般放到装机模板里,或者直接拿安装包部署一下就好了),至于是什么操作系统其实并不重要。
延续上文REST服务的例子,Java的Web项目一般都会部署到容器里,比如Tomcat或者Jetty,当然也有用商业容器的(很多银行就是用的WebLogic),所以大家就都认为部署Java程序需要先有容器,这其实是几年前的事情了,后来刮起了一股内嵌容器的风潮,Tomcat和Jetty都可以嵌入到你的程序里,再也不用为有没有容器而烦恼了。Spring Boot索性把这件事变得更简单了,mvn package
后,一句话就能搞定内置Tomcat的启动、完成各种部署,然后一切就变成下面这样(假设最后生成的Jar包名为demo.jar):
java -jar demo.jar
在Spring Boot 1.3里,还能通过调整Maven Plugin的配置,让Jar可以直接执行(不要小看这么一个变化,它可以大大提升可运维性):
./demo.jar
所以说Java程序难部署其实也是历史,现在的Java程序部署早已是另一番光景。两者的编译、打包、部署环节完全可以打成平手。笔者认为有些方面Java反而更胜一筹,比如Java基本就不用操心交叉编译的问题;Go的库在发布时推荐直接发布源码而非二进制包,遇到天朝特有的网络无法访问的情况,编译个东西还要自备梯子……至于和Nginx等等的配合,更是大家都很方便,就不再赘述了。
完成了部署,接下来的日志和监控,都是很常规的问题,日志各自有对应的库,而监控都是依赖专业的监控平台,自己做好信息输出就好了,请容我再秀一下Spring Boot的RemoteShell终端监控,除了常规的HTTP方式输出JSON信息(自带了健康检查、仪表数据、Dump、请求跟踪等一系列REST输出),还自带了这么个类似top
的高大上的玩意儿,ssh -p 2000 user@localhost
后执行dashboard
可以看到这个实时更新的界面。
总结
说了这么多,来总结下全文的观点——虽然Go在语言上表现的很出色,也融入了很多最佳实践,但是结合多方考虑,在很多情况下它并不会比Java带来更多价值,甚至还不一定能做的比Java好,因此作为一个Java程序员,我不会在自己的生产项目中转向Go。
此外,除了本文重点讨论的那些问题,还有更现实的问题摆在那里,比如团队转型成本和招聘的成本,千万不要小看招聘,对于管理者而言,招聘也是工作中的重要内容,试想一下,是招个有经验的Go程序员容易,还是招一个有经验的Java程序员容易,就算能招到一个会Go的正式员工,你能招到一个会Go的外包么,特别是在团队急需补充新鲜血液时,结果是显而易见的。
但这一切都不妨碍大家来学习Go,本文开头就已经表达过这一观点,业余时间学习Go和在生产项目中不用Go并不冲突,Go还是有很多值得学习和借鉴的地方,而且谁也说不准哪天你就真遇上了适合用Go的项目呢。
最后,特别感谢谢孟军与李道兵在本文写作过程中与笔者的各种思维碰撞与交流。