我签约将一个大型Java代码库移植到Go。
有问题的代码是RavenDB的Java客户端,一个NoSQL JSON文档数据库。测试代码约为5万行。
端口的结果是Go客户端。
本文介绍了我在此过程中学到的知识。
测试,代码覆盖
大型项目从自动化测试和跟踪代码覆盖中受益匪浅。
我使用TravisCI和AppVeyor进行测试。 Codecov.io用于代码覆盖。还有很多其他服务。
我使用了AppVeyor和TravisCI,因为一年前Travis没有Windows支持而AppVeyor没有Linux支持。
今天,如果我从头开始设置这个,我会坚持只使用AppVeyor,因为它现在可以进行Linux和Windows测试,TravisCI的未来是黑暗的,在被私募股权公司收购后据报道解雇了原来的开发团队。
Codecov勉强够用。对于Go,他们将非代码行(注释等)计为未执行。如工具所报告的那样,不可能获得100%的代码覆盖率。工作服似乎有同样的问题。
它总比没有好,但是有机会做得更好,特别是对于Go项目。
Go的竞争探测器很棒
部分代码使用并发性,并且很容易出现并发错误。
Go提供了在编译期间可以使用-race标志启用的竞争检测器。
它会降低程序的速度,但是额外的检查可以检测你是否同时修改了相同的内存位置。
我总是在启用-race的情况下运行测试,它提醒我很多竞争,这使我能够及时修复它们。
构建用于测试的自定义工具
在一个大项目中,通过检查无法验证正确性。太多的代码可以立刻保留在你的头脑中。
当测试失败时,从测试失败的信息中找出原因可能是一个挑战。
数据库客户端驱动程序通过HTTP与RavenDB数据库服务器通过JSON对命令和结果进行编码。
将Java测试移植到Go时,能够捕获Java客户端和服务器之间的HTTP流量并将其与Go端口生成的HTTP流量进行比较非常有用。
我建立了自定义工具来帮助我做到这一点。
为了捕获Java客户端中的HTTP流量,我在Go中构建了一个日志记录HTTP代理,并指示Java客户端使用该HTTP代理。
对于Go客户端,我在库中构建了一个允许拦截HTTP请求的钩子。我用它来记录流量到文件。
然后,我能够将Java客户端生成的HTTP流量与我的Go端口生成的流量进行比较,并发现差异。
移植过程
你不能只是以随机顺序开始移植5万行代码。没有经过每一小步后的测试和验证,我确信我会被复杂性打败。
我是RavenDB和Java代码库的新手。我的第一步是深入了解Java代码的工作原理。
客户端的核心是通过HTTP协议与服务器通信。我捕获了流量,查看了它并编写了最简单的Go代码来与服务器通信。
当它工作时,它给了我信心,我将能够复制功能。
我的第一个里程碑是移植足够的代码以便能够移植最简单的Java测试。
我使用了自下而上和自上而下的方法。
自下而上的部分是我在调用链底部识别代码的地方,负责向服务器发送命令并解析响应并移植它们。
自上而下的部分是我通过移植测试的地方,以确定需要移植代码的哪些部分来实现该部分。
在成功移植第一步之后,剩下的工作是一次移植一个测试,同时移植使测试工作所需的所有必要代码。
在测试被移植并通过之后,我做了一些改进,使代码更加Go-ish。
我相信这种循序渐进的方法对完成工作至关重要。
在心理上,当面对为期一年的项目时,拥有较小的中级里程碑非常重要。击中那些让我保持动力。
始终保持代码编译,运行和通过测试也很好。允许错误积累可能会使你很难在最终得到它时修复它们。
将Java移植到Go的挑战
该端口的目标是使其尽可能接近Java代码库,因为它需要在未来与Java更改保持同步。
我有点惊讶我以逐行方式移植了多少代码。端口中最耗时的部分是颠倒变量声明的顺序,从Java的类型名称到Go的名称类型。我希望有一个工具可以为我做那部分。
字符串与字符串
在Java中,String是一个实际上是引用(指针)的对象。因此,字符串可以为null。
在Go中,string是一种值类型。它不能是零,只能是空的。
这不是什么大不了的事,大部分时间我都可以用“”机械替换null。
错误与异常
Java使用异常来传达错误。
Go返回错误接口的值。
移植并不困难但它确实需要更改许多函数签名以返回错误值并将它们传播到调用堆栈。
泛型
Go还没有它们。
移植通用API是最大的挑战。
以下是Java中泛型方法的示例:
调用方式:
在Go中,我使用了两种策略。
一种是使用interface {},它结合了值及其类型,类似于Java中的对象。 这不是优选的方法。 虽然它可以工作,但在接口{}上操作对于库的用户来说是笨拙的。
在某些情况下,我能够使用反射,上面的代码被移植为:
我可以使用反射来查询结果类型,并从JSON文档创建该类型的值。
调用方法:
函数重载
Go没有它(很可能永远不会拥有它)。
我不能说我找到了一个很好的解决方案来移植那些。
在某些情况下,重载用于创建更短的帮助程序:
有时我会放弃较短的助手。
有时我会写2个函数:
当潜在参数的数量很大时,我有时会做:
继承
Go不是特别面向对象而且没有继承。
简单的继承案例可以通过嵌入移植。
有时可以移植为:
我们在B中嵌入了A,所以B继承了A的所有方法和字段。
它不适用于虚拟功能。
直接移植使用虚函数的代码没有好办法。
模拟虚函数的一个选项是使用嵌入结构和函数指针。这基本上重新实现了Java作为对象实现的一部分免费提供的虚拟表。
另一种选择是编写一个独立函数,通过使用类型开关为给定类型调度正确的函数。
接口
Java和Go都有接口,但它们是不同的东西,比如苹果和萨拉米香肠。
有几次我创建了一个复制Java接口的Go接口类型。
在更多情况下,我删除了接口,而是在API中暴露了具体的结构。
包之间的循环导入
Java允许在包之间进行循环导入。
go没有。
结果我无法在我的端口中复制Java代码的包结构。
为简单起见,我选择了一个包。不理想,因为它最终成为非常大的包装。事实上,Go 1.10无法在Windows上的单个软件包中处理如此多的源文件。幸运的是,它已在Go 1.11中得到修复。
私有,公共,受保护
Go的设计师不被重视。它们简化概念的能力是无与伦比的,访问控制就是其中的一个例子。
其他语言倾向于细粒度的访问控制:public,private,protected以尽可能小的粒度(按类字段和方法)指定。
因此,实现某些功能的库对使用该库的外部代码在同一库中的其他类具有相同的访问权限。
通过仅对包级别进行公共与私有和范围访问来简化。
这更有意义。
当我写一个库,比如解析markdown时,我不想将实现的内部暴露给库的用户。但是将这些内部隐藏起来会适得其反。
Java程序员注意到这个问题,有时使用接口作为修复过度暴露类的***。通过返回接口而不是具体类,你可以隐藏一些可用于指导该类用户的公共API。
并发
Go的并发性是最好的,内置的竞争检测器在排除并发错误方面有很大帮助。
话虽这么说,在我的第一个移植传递中,我选择了模拟Java API。例如,我实现了Java的CompletableFuture类的副本。
只有在代码工作之后,我才会将其重新构造为更具惯用性的Go。
流畅的功能链
RavenDB具有非常复杂的查询功能。 Java客户端使用方法链来构建查询:
这仅适用于通过异常传达错误的语言。 当函数另外返回错误时,不再可能将其链接起来。
要在Go中复制链接,我使用了“状态错误”方法:
这可以链接:
JSON编组
Java没有内置编组,客户端使用Jackson JSON库。
Go在标准库中具有JSON支持,但它没有为调整编组过程提供尽可能多的钩子。
我并没有尝试匹配Java的所有功能,因为Go提供的内置JSON支持似乎足够灵活。
Go代码更短
这不是Java的一个属性,而是决定什么是惯用代码的文化。
在Java中,setter和getter方法很常见。 因此,Java代码:
在go中:
3行与11行。 当你有很多有很多成员的类时,它确实会加起来。
大多数其他代码最终具有相同的长度。郑州不孕不育:zzfkyy120.com/郑州不孕不育医院×××:zztjyy120.800400.net/
有疑问加站长微信联系(非本文作者)