写点什么

作为一名 Java 程序员,我为什么不在生产项目中转向 Go

  • 2015-09-22
  • 本文字数:6642 字

    阅读完需:约 22 分钟

自 Google 在 2009 年发布 Go 语言的第一个正式版之后,这门语言就以出色的语言特性受到大家的追捧,尤其是在需要高并发的场景下,大家都会想到是不是该用 Go。随后,在国内涌现出了一批以七牛为代表的使用 Go 作为主要语言的团队,而许式伟大神本人也在各种场合下极力推动 Go 在国内的发展,于是在这种大环境下,中国的 Go 开发者群体逐渐超越了其他地区

那么问题来了,业余时间好学是一回事,真正要将一个新东西运用到生产中则是另一回事。JavaScript 的开发者可以义无反顾地选择 Node.js,但是对于 Java 开发者来说,在下一个大项目里究竟是该选择 Go,还是 Java 呢?

郑重声明:本文并不是来探讨 Go 或者 Java 谁是更好的语言,每种语言都有自己的设计哲学和适用场景,今天主要是在探讨实际工程中的选择和权衡的问题,所以请不要上纲上线。

语言本身

首先,需要说明一下,作为一个技术决策者,在进行技术选型时并不能单方面地根据语言本身的特点直接下结论。实际情况下,大多数人会使用一系列的框架、库及工具,简而言之就是会考虑很多周边生态环境的因素,同时还要结合公司的特点、各种历史问题和实际客观因素等等一系列的考虑点综合下来才能完成决策。所以,接下来我们先从语言开始,一步一步来分析下在你的项目中选择 Go 是否合适。

Go 在高并发编程方面无疑是出众的,通过 goroutine 从语言层面支持了协程,这是 Java 等语言所无法比拟的,这也是大多数人在面对高并发场景选择 Go 的重要原因之一。虽然 Java 有 Kilim 之类的框架,但没有语言层的支持始终稍逊一筹。

除此之外,Go 的其他语法也很有趣,比如多返回值,在一定程度上为开发者带来了一定的便利性。试想,为了返回两到三个值,不得不封装一个对象,或者抹去业务名称使用MapList等集合类,高级一点用 Apache 的PairTriple,虽然可行,但始终不如 Go 的实现来得优雅。在此之上,Go 也统一了异常的返回方式,不用再去纠结是通过抛异常还是错误码来判断是否成功,多返回值的最后一个是Error就行了。

Go 在语言的原生类型中支持了常用的一些结构,比如mapslice,而其他语言中它们更多是存在于库中,这也体现了这门语言是从实践角度出发的特点,既然人人都需要,为什么不在语言层面支持它呢。函数作为一等公民出现在了 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 方面,MavenGradle都是注重于工程生命周期管理的工具,而且Maven更是历史悠久,被广泛用于各种项目之中。以Maven为例,不仅能够实现上述所有功能,还有很强的插件扩展能力,这里需要的只是一次性维护好pom.xml文件就行了,由于Maven的使用群很大,网上有大量的范例,甚至还有很多生成工程的工具和模板,所以使用成本并不高。

这里还要衍生出一个话题,就是依赖管理,在开发代码时,势必需要依赖很多外部的东西,Go 可以直接import远程的内容,这个特性很有创意,但并不能很好地解决版本的问题,在MavenGradle里,我们可以直接指定各个依赖项甚至是插件的版本,工具会自动从仓库中下载它们。如果需要同时在同一个系统的不同模块里依赖同一个库的不同版本,我们还能够通过 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 的项目呢。

最后,特别感谢谢孟军与李道兵在本文写作过程中与笔者的各种思维碰撞与交流。

2015-09-22 18:4724097
用户头像

发布了 135 篇内容, 共 60.6 次阅读, 收获喜欢 43 次。

关注

评论

发布
暂无评论
发现更多内容

云原生大数据平台零信任网络安全实践技术稿

星环科技

详解HashMap源码解析(下)

Jeremy Lai

HashMap底层原理

【JVM故障问题排查心得】「内存诊断系列」JVM内存与Kubernetes中pod的内存、容器的内存不一致所引发的OOMKilled问题总结(下)

洛神灬殇

JVM 12月日更 堆外内存 内存干扰

五种方式实现 Java 单例模式

Jeremy Lai

设计模式 单例模式

SpringBoot快速整合通用Mapper

Jeremy Lai

通用Mapper spring-boot

【0.1mm钻孔】揭秘激光钻孔加工常见的4种方法

华秋PCB

工艺 PCB PCB设计 钻孔

SAP MM 使用两个STO实现免关税跨国公司间转储(II)

SAP虾客

SAP MM PO输出 NACE

Zebec节点Zepoch销量接近800个,生态发展信心十足

股市老人

MASA Framework 事件总线 - 跨进程事件总线

MASA技术团队

.net MASA Framewrok MASA

Serverless 架构下的 AI 应用开发

Serverless Devs

【JUC】循环屏障CyclicBarrier详解

JAVA旭阳

Java 并发 JUC

模拟HashMap冲突

Jeremy Lai

hashmap 哈希冲突

旺链科技荣膺“2022上海软件核心竞争力企业”

旺链科技

区块链 产业区块链 12 月 PK 榜

web前端培训程序员学习怎么提升技术

小谷哥

【JVM故障问题排查心得】「内存诊断系列」JVM内存与Kubernetes中pod的内存、容器的内存不一致所引发的OOMKilled问题总结(上)

洛神灬殇

Docker Kubernetes 容器 JVM 12月日更

Java递归实现评论多级回复

Jeremy Lai

递归 解决方案

云小课|云小课带你玩转可视化分析ELB日志

华为云开发者联盟

云计算 后端 华为云 12 月 PK 榜

上海大数据培训机构怎么选择比较好

小谷哥

一种多引擎可视化数据流实现方案

元年技术洞察

数据中台 数字化转型 专利解析 方舟企业数字化 PaaS 平台

将 Vue.js 项目部署至静态网站托管,并开启 Gzip 压缩

华为云开发者联盟

开发 华为云 12 月 PK 榜

meta force原力元宇宙魔豹联盟矩阵公排dapp系统开发源代码

开发微hkkf5566

打造无证服务化:这个政务服务平台有点不一样

华为云开发者联盟

区块链 华为云 12 月 PK 榜

大数据培训学习分析师怎么样

小谷哥

零售品牌如何挑选达人KOL?火山引擎VeDI有妙招!

字节跳动数据平台

大数据 营销 营销数字化 12 月 PK 榜

瓴羊Quick BI带给企业新发展,企业数字营销和运营变得格外简单

对不起该用户已成仙‖

详解HashMap源码解析(上)

Jeremy Lai

HashMap底层原理

web前端培训程序员学习什么呢

小谷哥

大数据培训学习编程技术的方法有哪些

小谷哥

React 之 createElement 源码解读

冴羽

JavaScript 前端 前端框架 React react源码

刷新AI作图速度,最快开源Stable Diffusion出炉

OneFlow

人工智能 深度学习 前沿技术

作为一名Java程序员,我为什么不在生产项目中转向Go_Java_丁雪丰_InfoQ精选文章