当前位置: 首页 > 科技观察

将5万行Java代码移植到Go的经验教训

时间:2023-03-20 14:37:51 科技观察

我曾经有一份合同,负责将大型Java代码库移植到Go。此代码是RavenDB的Java客户端,RavenDB是一个NoSQLJSON文档数据库。包括测试代码在内,总共约有50,000行。端口的结果是一个Go客户端。本文描述了我在这次迁移过程中学到的东西。测试、代码覆盖率自动化测试和代码覆盖率跟踪可以极大地有益于大型项目。我使用TravisCI和AppVeyor进行测试。Codecov.io用于检测代码覆盖率。还有许多其他类似的服务。我同时使用AppVeyor和TravisCI,因为Travis一年前就停止支持Windows而AppVeyor不支持Linux。如果我现在重新审视这些工具,我只会使用AppVeyor,因为它现在支持在Linux和Windows平台上进行测试,而TravisCI在被私募股权公司收购并解雇了最初的开发团队后前途未卜。Codecov几乎无法进行代码覆盖测试。对于Go,它将非代码行(例如注释)视为未实现的代码。使用此工具不可能获得100%的代码覆盖率。工作服似乎也有同样的问题。少总比没有好,但是这些工具可以让事情变得更好,尤其是对于围棋程序。Go的比赛检测很棒。部分代码使用了并发,并发容易出错。Go提供了一个竞争检测器,可以在编译时使用-race标志启用。它会减慢程序的速度,但额外的检查可以检测是否同时修改了相同的内存位置。我在运行测试时始终打开-race,通过它的警报,我可以快速解决这些比赛问题。构建用于测试的特定工具大型项目很难通过目视检查来验证正确性。代码太多,您的大脑很难一下子记住它们。当测试失败时,仅从测试失败的信息中查找原因也是一个挑战。数据库客户端驱动使用HTTP协议与RavenDB数据库服务器连接,传输的命令和响应结果以JSON编码。将Java测试代码移植到Go时,如果您可以获得Java客户端和服务器HTTP流量并将其与移植到Go的代码生成的HTTP流量进行比较,那么获得此信息将很有用。我构建了一些特定的工具来为我做这件事。为了抓取Java客户端的HTTP流量,我用Go搭建了一个日志记录HTTP代理,Java客户端通过这个代理与服务端进行交互。对于Go客户端,我构建了一个拦截HTTP请求的钩子。我用它来记录流量到一个文件。然后我能够比较Java客户端与Go移植客户端生成的HTTP流量的差异。您不能在移植过程中随意开始迁移50,000行代码。我敢肯定,如果我不在每个小步骤之后进行测试和验证,我就会被整体代码的复杂性打败。我是RavenDB和Java代码库的新手。所以我的第一步是深入了解这段Java代码是如何工作的。客户端的核心是通过HTTP协议与服务器进行交互。我捕获并研究了流量,编写了最简单的Go代码来与服务器交互。当这有效时,我相信我可以复制该功能。我的第一个里程碑是移植足够多的代码以通过移植最简单的Java测试代码的测试。我结合使用了自下而上和自上而下的方法。自下而上的部分是我定位并移植了调用链底部的代码,用于向服务器发送命令并解析响应。从上到下的部分是指我一步步跟踪要移植的测试代码,确定需要移植的功能代码部分。在成功完成第一个移植后,剩下的就是一次移植一个测试,以及通过该测试所需的所有代码。当测试被移植并且测试通过时,我做了一些改进,使代码更像Go。我相信这种循序渐进的方法对于完成移植工作非常重要。从心理学的角度来看,面对长期项目时,设定短期的中间里程碑很重要。不断达到这些里程碑让我充满动力。让您的代码始终保持可编译、可运行和可测试也很好。当您最终不得不处理那些累积的缺陷时,将很难修复它们。将Java移植到Go的挑战移植的目标是尽可能与Java代码库保持一致,因为移植的代码需要跟上Java未来的变化。有时我对我逐行移植的代码量感到惊讶。在移植过程中,最耗时的部分是反转变量的声明顺序。Java的声明顺序是typename,Go的声明顺序是nametype。我真的希望有一个工具可以为我完成这部分工作。字符串与字符串在Java中,字符串是一个本质上是引用(指针)的对象。因此,字符串可以为null。字符串是Go中的值类型。它不能是nil,只是空的。这没什么大不了的,大多数时候我可以用""替换null。错误与异常Java使用异常来传达错误。Go返回错误接口的值。移植并不困难,但需要大量函数签名更改以支持返回错误值并在调用堆栈上传播它们。泛型Go(目前)不支持泛型。移植通用接口是最大的挑战。下面是Java中通用方法的示例:publicTload(Classclazz,Stringid){Caller:Foofoo=load(Foo.class,"id")在Go中,我使用两种策略。其中之一是使用interface{},它由一个值和一个类型组成,类似于Java中的对象。不推荐使用此方法。虽然有效,但操作interface{}不适合此库的用户。在某些情况下我可以使用反射,上面的代码可以移植为:funcLoad(resultinterface{},idstring)error我可以使用反射来获取结果的类型,然后从JSON文档中创建一个该类型的值。调用方代码:varresult*Fooerr:=Load(&result,"id")函数重载Go不支持(很可能永远不会)函数重载。我不确定我是否找到了移植此类代码的正确方法。在某些情况下,重载用于创建更短的辅助函数:voidfoo(inta,Stringb){}voidfoo(inta){foo(a,null);}有时我只是删除更短的辅助函数。有时我会写两个函数:funcfoo(aint){}funcfooWithB(aint,bstring){}当潜在参数的数量很大时,有时我会这样做:typeFooArgsstruct{AintBstring}funcfoo(args*FooArgs){}继承Go不是一个面向对象的语言,没有继承。简单情况下的继承可以使用嵌套方法进行迁移。classB:A{}有时可以移植为:typeAstruct{}typeBstruct{A}我们将A嵌入到B中,所以B继承了A的所有方法和字段。这种方式对虚函数不起作用。没有好的方法来移植使用虚函数的代码。模拟虚函数的一种方法是嵌套结构和函数指针。这本质上是Java作为对象实现的一部分免费提供的虚拟表的重新实现。另一种方法是编写一个单独的函数,使用类型判断为给定类型分派正确的函数。接口Java和Go都有接口,但它们是不同的东西,就像苹果和意大利腊肠。在极少数情况下,我会创建Go的接口类型来复制Java接口。大多数时候,我放弃使用接口,而是在API中公开具体结构。依赖包的循环导入Java允许循环导入包。Go不允许这样做。结果,我无法在端口中复制Java代码的包结构。为了简化,我使用了一个包。这种方法并不理想,因为包会变得非常臃肿。事实上,这个包非常臃肿,Windows上的Go1.10无法在一个包中处理那么多的源文件。幸运的是,Go1.11修复了这个问题。private、public、protectedGo的设计者被低估了。他们简化概念的能力是独一无二的,访问控制就是一个例子。其他语言倾向于细粒度的权限控制:以尽可能小的粒度指定public、private和protected(每个类字段和方法)。结果是当外部代码使用这个库时,这个库实现的一些函数和这个库中的其他类具有相同的访问权限。Go简化了这个概念,只有public和private,访问范围仅限于包级别。这更有意义。当我想写一个库,比如解析markdown,我不想把内部实现暴露给这个库的使用者。但是为了对自己隐藏这些内部实现,效果恰恰相反。Java开发人员注意到了这个问题,有时使用接口作为修复过度暴露类的技巧。通过返回接口而不是具体类,此类的消费者看不到一些可用的公共接口。并发简单的说Go的并发是最好的,内置的racedetector对解决并发问题很有帮助。正如我之前所说,我做的第一个移植是模拟Java接口。例如,我实现了JavaCompletableFuture类的副本。只有在代码可运行后,我才会重新组织代码以使其更像Go。平滑的函数链调用RavenDB具有复杂的查询能力。Java客户端使用链式方法构建查询:Listresults=session.query(User.class).groupBy("name").selectKey().selectCount().orderByDescending("count").ofType(ReduceResult.class).toList();链接仅在错误通过异常交互的语言中有效。当一个函数另外返回一个错误时,就不可能像上面那样进行链式调用。为了在Go中复制链接,我使用了“状态错误”方法:typeQuerystruct{errerror}func(q*Query)WhereEquals(fieldstring,valinterface{})*Query{ifq.err!=nil{returnq}//logicthatmightsetq。errreturnq}func(q*Query)GroupBy(fieldstring)*Query{ifq.err!=nil{returnq}//logicthatmightsetq.errreturnq}func(q*Query)Execute(resultinterface{})error{ifq.err!=nil{returnq.err}//dologic}链式调用可以这样写:varresult*Fooerr:=NewQuery().WhereEquals("Name","Frank").GroupBy("Age").Execute(&result)JSON解析Java没有内置JSON解析功能,客户端使用JacksonJSON库。Go在标准库中有JSON支持,但是它没有提供足够的钩子函数来展示JSON解析的过程。我没有尝试匹配所有Java特性,因为Go的内置JSON支持似乎足够灵活。越来越短的Go代码不是Java的属性,而是为该语言编写惯用代码的文化的属性。在Java中,setter和getter方法非常普遍。例如Java代码:classFoo{privateintbar;publicvoidsetBar(intbar){this.bar=bar;}publicintgetBar(){returnthis.bar;}}Go语言版本如下:typeFoostruct{Barint}3行vs11行。当您有大量成员众多的班级时,这可以使班级相加。大多数其他代码***长度都差不多。使用Notion来组织我的工作我是Notion.so的重度用户。简单来说,Notion是一款多层次的笔记应用。可以将其视为Evernote和wiki的结合,由顶级软件设计师精心设计和实施。下面是我如何使用Notion来组织我的Go移植工作:下面是具体细节:我有一个页面没有在上面显示,它带有一个日历视图,用于记录我正在做什么以及我在特定时间花费了多少时间。因为这个合同是按小时计费的,工作时间的统计是非常重要的信息。多亏了这些笔记,我知道我在11个月内花了601个小时在这个开发上。客户喜欢知道发生了什么。我有一个记录每月工作总结的页面,如下所示:这些页面与客户共享。在开始每天的工作时,短期待办事项列表很有用。我什至使用概念页面来管理发票,使用“导出为PDF”功能生成发票的PDF版本。招聘Go程序员你的公司还需要Go开发人员吗?你可以雇用我其他资源对于问题,我提供一些额外的说明:HackerNewsdiscussion/r/golangdiscussion其他资源:如果你需要一个NoSQL、JSON文档数据库,你可以试试RavenDB。它具有一整套高级功能。如果您使用Go编程,则可以免费阅读EssentialGo编程书籍。如果您对Notion感兴趣,我是Notion的高级用户***:我对NotionAPI进行了逆向工程我为NotionAPI编写了一个非官方的Go库本网站上的所有内容都是使用Notion编写的,并使用我进行了定制工具链发布。