我签了一个合同,移植一个大的Java代码库去。
有问题的代码是RavenDB的Java客户端,一个NoSQL JSON文档数据库。测试代码大概5万行。

端口的结果是Go客户端。
这篇文章介绍了我在这个过程中学到的知识。
测试,代码覆盖率
大型项目从自动化测试和跟踪代码覆盖率中受益匪浅。
我用TravisCI和AppVeyor进行测试。Codecov.io用于代码覆盖率。还有很多其他服务。
我用AppVeyor和TravisCI是因为一年前Travis没有Windows支持,AppVeyor没有Linux支持。
今天,如果我从头开始设置,我将坚持只使用AppVeyor,因为它现在可以在Linux和Windows上测试。特拉维斯奇前途一片黑暗,有消息称他在被私募股权公司收购后解雇了原开发团队。
Codecov勉强够用。对于Go,他们将非代码行视为未执行。正如工具所报告的,不可能实现100%的代码覆盖率。工作服似乎也有同样的问题。
聊胜于无,但有机会做得更好,尤其是对于围棋项目。
围棋的竞争检测器很棒。
部分代码使用了并发,容易出现并发错误。
Go提供了一个竞争检测器,可以在编译期间使用-race标志启用它。
它会降低程序的速度,但是额外的检查可以检测出你是否同时修改了同一个内存位置。
我总是在启用-race的情况下运行测试。它让我想起了许多比赛,这使我能够及时修复它们。
构建定制的测试工具
在大型项目中,正确性无法通过检查来验证。太多的代码可以一下子留在脑子里。
当测试失败时,从测试失败的信息中找出原因可能是一个挑战。
客户端驱动通过HTTP和RavenDB数据库服务器通过JSON对命令和结果进行编码。
将Java测试移植到Go时,捕捉Java客户端和服务器之间的HTTP流量,并与Go端口生成的HTTP流量进行比较,这是非常有用的。
我设置了自定义工具来帮助我做到这一点。
为了在Java客户端捕获HTTP流量,我在Go中构建了一个日志HTTP代理,并指示Java客户端使用HTTP代理。
对于Go client,我在库中构建了一个钩子,允许阻塞HTTP请求。我用它来记录文件流量。
然后,我将Java客户端生成的HTTP流量与我的Go端口生成的流量进行了比较,发现了不同之处。
移植过程
你不能随便开始移植50,000行代码。如果每一小步都没有经过测试和验证,我肯定会被复杂性打败。
我是RavenDB和Java代码库的新手。我的第一步是深入理解Java代码是如何工作的。
客户端的核心是通过HTTP协议与服务器进行通信。我捕获了流量,查看了它,并编写了最简单的Go代码来与服务器通信。
当它工作时,它给了我信心,我将能够复制函数。
我的第一个里程碑是移植足够的代码,以便能够移植最简单的Java测试。
我使用了自下而上和自上而下的方法。
自底向上部分是我识别调用链底部代码的地方,负责向服务器发送命令,解析响应并移植它们。
自顶向下的部分是我通过移植测试的地方,以确定需要移植代码的哪些部分来实现这一部分。
在成功迁移的第一步之后,剩下的工作是一次迁移一个测试,同时迁移使测试工作所需的所有必要代码。
在测试被移植并通过之后,我做了一些改进,使代码更加流畅。
我相信这种循序渐进的方法对完成这项工作是必不可少的。
从心理学上来说,当面对一个长达一年的项目时,有更小的中间里程碑是非常重要的。这些点击让我保持动力。
让代码一直编译、运行并通过测试也是很好的。允许错误累积可能会让你在最终得到错误时很难修复它们。
将Java移植到Go的挑战
这个端口的目标是让它尽可能接近Java代码库,因为它需要在未来跟上Java的变化。
我有点惊讶自己一行一行移植了这么多代码。port最费时间的部分是颠倒变量声明的顺序,从Java的类型名到Go的名称类型。我希望有一种工具可以帮我完成这一部分。
绳子和绳子
在Java中,String是一个对象,它实际上是一个引用。因此,该字符串可以为空。
在Go中,string是一种值类型。不能是零,只能是空。
没什么大不了的。很多时候,我可以用" " machine代替null。
错误和异常
使用Java异常来传达错误。
Go返回错误接口的值。
移植并不困难,但是它需要改变许多函数签名来返回错误值并将它们传播到调用堆栈。
无商标消费品
Go还没有它们。
移植通用API是最大的挑战。
以下是Java中泛型方法的示例:
呼叫方法:

在围棋中,我使用了两种策略。
一种是使用接口{},它将值和它们的类型结合起来,类似于Java中的对象。这不是首选方法。虽然它可以工作,但是对库的用户来说,在接口{}上操作是笨拙的。
在某些情况下,我能够使用反射,上面的代码被移植为:
我可以使用反射来查询结果类型,并从JSON文档中创建该类型的值。
调用方法:
功能过载
不要它。
我不能说我找到了移植它们的好方法。
在某些情况下,重载用于创建较短的辅助对象:
有时我会放弃我的矮个子助手。
有时我写两个函数:
当潜在参数的数量很大时,我有时会这样做:
继承
Go不是特别面向对象,没有继承。
简单的继承案例可以通过嵌入来移植。
有时它可以被移植为:
我们在B中嵌入了A,所以B继承了A的所有方法和字段。
它不适用于虚函数。
没有好的办法直接移植使用虚函数的代码。
模拟虚函数的一个选择是使用嵌入式结构和函数指针。这基本上重新实现了Java免费提供的虚拟表,作为对象实现的一部分。
另一种选择是编写一个独立的函数,并通过使用类型开关为给定的类型调度正确的函数。
港口
Java Go和Java Go都有接口,但是是不同的东西,比如苹果和腊肠。
好几次,我创建了一个复制Java接口的Go接口类型。
更多情况下,我删除了接口,但是在API中暴露了具体的结构。
包之间的循环导入
Java允许在包之间循环导入。
去吧,不。
结果就是我的端口里不能复制Java代码的包结构。
为了简单起见,我选择了一个包。不理想,因为它最终会变成一个非常大的包。事实上,Go 1.10在Windows上单个包中无法处理这么多源文件。好在在Go 1.11中已经修复。
私有、公共和受保护
Go的设计师不受重视。他们简化概念的能力是无与伦比的,访问控制就是其中之一。
其他语言倾向于细粒度的访问控制:public、private和protected以尽可能小的粒度指定。
因此,实现某些函数的库对同一库中使用该库外部代码的其他类具有相同的访问权限。
仅通过公共和私有进行简化,并在包级别确定访问范围。
这更有道理。
当我编写一个库时,比如解析markdown,我不想向库的用户公开实现的内部。但是隐藏这些内幕会适得其反。
Java程序员注意到了这个问题,有时会像黑客一样使用接口来修复过度暴露的类。通过返回接口而不是具体的类,你可以隐藏一些公共的API,这些API可以用来指导这个类的用户。
被...复杂化
Go的并发性是最好的,内置的竞跑检测器对消除并发错误很有帮助。
说到这里,在我的第一次移植过程中,我选择模拟Java API。例如,我实现了Java的CompletableFuture类的副本。
只有在代码运行之后,我才会把它重新构建成一个更习惯的Go。
光滑功能链
RavenDB有一个非常复杂的查询函数。Java客户端使用方法链来构建查询:
这只适用于通过异常传递错误的语言。当函数返回另一个错误时,就不能再链接它了。
为了复制Go中的链接,我使用了“状态错误”方法:
这可以联系到:
JSON分组
Java没有内置的编组,客户端使用Jackson JSON库。
Go在标准库中有JSON支持,但是它没有提供尽可能多的钩子来调整分组过程。
我没有试图匹配Java的所有功能,因为Go提供的内置JSON支持看起来足够灵活。
Go代码更短

这不是Java的属性,而是决定什么是惯用代码的文化。
Setter和getter方法在Java中很常见。因此,Java代码:
在go中:
3线11线。当你有很多类,有很多成员时,它就增加了。
大多数其他代码都以相同的长度结束。


