[译] 将 5 万行 Java 代码移植到 Go 学到的经验

标签: tuicool | 发表时间:2019-04-19 00:00 | 作者:
出处:http://itindex.net/relian
  • 原文地址:

    Lessons learned porting 50k loc from Java to Go

  • 原文作者:Krzysztof Kowalczyk

  • 译文出处: https://blog.kowalczyk.info

  • 本文永久链接:

    https://github.com/gocn/translator/blob/master/2019/w15_lessions_learned_porting_50k_loc_from_java_to_go_translation.md

  • 译者:cvley

  • 校对:Ryan

我曾经签订了一个把大型的 Java 代码库迁移至 Go 的工作合同。

这份代码是 RavenDB 这一 NoSQL JSON 文档数据库的 Java 客户端。包含测试代码,一共有约 5 万行。

移植的结果是一个 Go 的客户端。

本文描述了我在这个迁移过程中学到的知识。

测试,代码覆盖率

自动化测试和代码覆盖率追踪,可以让大型项目获益匪浅。

我使用 TravisCI 和 AppVeyor 进行测试。Codecov.io 用来检测代码覆盖率。还有许多其他的类似服务。

我同时使用 AppVeyor 和 TravisCI,是因为 Travis 在一年前不再支持 Windows,而 AppVeyor 不支持 Linux。

如果现在让我重新选择这些工具,我将只使用 AppVeyor,因为它现在支持 Linux 和 Windows 平台的测试,而 TravisCI 在被私募股权公司收购并炒掉原始开发团队后,前景并不明朗。

Codecov 几乎无法胜任代码覆盖率检测。对于 Go,它将非代码的行(比如注释)当做是未执行的代码。使用这个工具不可能得到 100% 的代码覆盖率。Coveralls 看起来也有同样的问题。

聊胜于无,但这些工具可以让情况变得更好,尤其是对 Go 程序而言。

Go 的竞态检测非常棒

一部分代码使用了并发,而并发很容易出错。

Go 提供了竞态检测器,在编译时使用 -race字段可以开启它。

它会让程序变慢,但额外的检查可以探测是否在同时修改同一个内存位置。

我一直开启 -race运行测试,通过它的报警,我可以很快地修复那些竞争问题。

构建用于测试的特定工具

大型项目很难通过肉眼检查验证正确性。代码太多,你的大脑很难一次记住。

当测试失败时,仅从测试失败的信息中找到原因也是一个挑战。

数据库客户端驱动与 RavenDB 数据库服务端使用 HTTP 协议连接,传输的命令和响应的结果使用 JSON 编码。

当把 Java 测试代码移植到 Go 时,如果可以获取 Java 客户端与服务端的 HTTP 流量,并与移植到 Go 的代码生成的 HTTP 流量对比,这个信息将非常有用。

我构建了一些特定的工具,帮我完成这些工作。

为了获取 Java 客户端的 HTTP 流量,我使用 Go 构建了一个 logging HTTP 代理,Java 客户端使用这个代理与服务端交互。

对于 Go 客户端,我构建了一个可以拦截 HTTP 请求的钩子。我使用它把流量记录在文件中。

然后我就可以对比 Java 客户端与 Go 移植的客户端生成的 HTTP 流量的区别了。

移植的过程

你不能随机开始迁移 5 万行代码。我确信,如果每一个小步骤之后不进行测试和验证的话,我都会被整体代码的复杂性给打败。

对于 RavenDB 和 Java 代码库,我是新手。所以我的第一步是深入理解这份 Java 代码的工作原理。

客户端的核心是与服务端通过 HTTP 协议交互。我捕获并研究了流量,编写最简单的与服务器交互的 Go 代码。

当这么做有效果之后,我自信可以复制这些功能。

我的第一个里程碑是移植足够的代码,可以通过移植最简单的 Java 测试代码的测试。

我使用了自底向上和自上到下结合的方法。

自底向上的部分是指,我定位并移植那些用于向服务器发送命令和解析响应的调用链底层的代码。

自上到下的部分是指,我逐步跟踪要移植的测试代码,来确定需要移植实现的功能代码部分。

在成功完成第一步移植后,剩下的工作就是一次移植一个测试,同时移植可通过这个测试的所有需要的代码。

当测试移植并测试通过后,我做了一些让代码更加 Go 风格的改进。

我相信这种一步一步渐进的方法,对于完成移植工作是很重要的。

从心理学角度来看,在面对一个长年累月的项目时,设置简短的中间态里程碑是很重要的。不断的完成这些里程碑让我干劲十足。

一直让代码保持可编译、可运行和可通过测试的状态也很好。当最终要面对那些日积月累的缺陷时,你将很难下手解决。

移植 Java 到 Go 的挑战

移植的目标是要尽可能与 Java 代码库一致,因为移植的代码需要与 Java 未来的变化保持同步。

有时我吃惊于自己以一行一行的方式移植的代码量。而移植过程中,最耗费时间的部分是颠倒变量的声明顺序,Java 的声明顺序是 type name,而 Go 的声明顺序是 name type。我真心希望有工具可以帮我完成这部分工作。

String vs. string

在 Java 中, String是一个本质上是引用(指针)的对象。因此,字符串可以为  null

在 Go 中 string是一个值类型。它不可能是  nil,仅仅为空。

这并不是什么大问题,大多情况下我可以无脑地将 null替换为 ""

Errors vs. exceptions

Java 使用异常来传递错误。

Go 返回 error接口的值。

移植不难,但需要修改大量的函数签名,来支持返回错误值并在调用栈上传播。

泛型

Go (目前)并不支持泛型。

移植泛型的接口是最大的挑战。

下面是 Java 中一个泛型方法的例子:

public <T> T load(Class<T> clazz, String id) {

调用者:

Foo foo = load(Foo.class, "id")

在 Go 中,我使用两种策略。

其中之一是使用 interface{},它由值和类型组成,与 Java 中的 object类似。不推荐使用这种方法。虽然有效,但对于这个库的用户而言,操作  interface{}并不恰当。

在一些情况下我可以使用反射,上面的代码可以移植为:

func Load(result interface{}, id string) error

我可以使用反射来获取 result的类型,再从 JSON 文档中创建这个类型的值。

调用方的代码:

var result *Foo
err := Load(&result, "id")

函数重载

Go 不支持(很大可能永远不会支持)函数重载。

我不确定我是否找到了正确的方式来移植这种代码。

在一些情况下,重载用于创建更简短的帮助函数:

void foo(int a, String b) {}
void foo(int a) { foo(a, null); }

有时我会直接丢掉更简短的帮助函数。

有时我会写两个函数:

func foo(a int) {}
func fooWithB(a int, b string) {}

当潜在的参数数量很大时,有时我会这么做:

type FooArgs struct {
    A int
    B string
}
func foo(args *FooArgs) { }

继承

Go 并不是面向对象语言,没有继承。

简单情况下的继承可以使用嵌套的方法移植。

class B : A { }

有时可以移植为:

type A struct { }
type B struct {
    A
}

我们把 A嵌入到 B中,因此 B继承了 A所有的方法和字段。

这种方法对于虚函数无效。

并没有好方法移植那些使用虚函数的代码。

模拟虚函数的一个方式是将结构体和函数指针嵌套。这本质上来说,是重新实现了 Java 免费提供的,作为 object实现一部分的虚表。

另一种方式是写一个独立的函数,通过类型判断来调度给定类型的正确函数。

接口

Java 和 Go 都有接口,但它们是不一样的内容,就像苹果和意大利香肠的区别一样。

在很少的情况下,我确实会创建 Go 的接口类型来复制 Java 接口。

大多数情况下,我放弃使用接口,而是在 API 中暴露具体的结构体。

依赖包的循环引入

Java 允许包的循环引入。

Go 不允许。

结果就是,我无法在移植中复制 Java 代码的包结构。

为了简化,我使用一个包。这种方法不太理想,因为这个包最后会变得很臃肿。实际上,这个包臃肿到在 Windows 下 Go 1.10 无法处理单个包内的那么多源文件。幸运的是,Go 1.11 修复了这个问题。

私有(private)、公开(public)、保护(protected)

Go 的设计师们被低估了。他们简化概念的能力是无与伦比的,权限控制就是其中的一个例子。

其他语言倾向于细粒度的权限控制:(每个类的字段和方法)指定最小可能粒度的公开、私有和保护。

结果就是当外部代码使用这个库时,这个库实现的一些功能和这个库中其他的类有一样的访问权限。

Go 简化了这个概念,只拥有公开和私有,访问的范围限制在包的级别。

这更合理一些。

当我想要写一个库,比如说,解析 markdown,我不想把内部实现暴漏给这个库的使用者。但对于我自己隐藏这些内部实现,效果恰恰相反。

Java 开发者注意到这个问题,有时会使用接口作为修复过度暴漏的类的技巧。通过返回一个接口,而不是具体的类,这个类的使用者就无法看到一些可用的公开接口。

并发

简单来说,Go 的并发是最好的,内建的竞态检测器非常有助于解决并发的问题。

我刚才说过,我进行的第一个移植是模拟 Java 接口。比如,我实现了 Java CompletableFuture类的复制。

只有在代码可以运行后,我才会重新组织代码,让代码更加符合 Go 的风格。

流畅的函数链式调用

RavenDB 拥有复杂的查询能力。Java 客户端使用链式方法构建查询:

List<ReduceResult> results = session.query(User.class)
                        .groupBy("name")
                        .selectKey()
                        .selectCount()
                        .orderByDescending("count")
                        .ofType(ReduceResult.class)
                        .toList();

链式调用仅在通过异常进行错误交互的语言中有效。当一个函数额外返回一个错误,就没法向上面那样进行链式调用。

为了在 Go 中复制链式调用,我使用了一个“状态错误(stateful error)”的方法:

type Query struct {
    err error
}

func (q *Query) WhereEquals(field string, val interface{}) *Query {
    if q.err != nil {
        return q
    }
    // logic that might set q.err
    return q
}

func (q *Query) GroupBy(field string) *Query {
    if q.err != nil {
        return q
    }
    // logic that might set q.err
    return q
}

func (q *Query) Execute(result inteface{}) error {
    if q.err != nil {
        return q.err
    }
    // do logic
}

链式调用可以这么写:

var result *Foo
err := NewQuery().WhereEquals("Name", "Frank").GroupBy("Age").Execute(&result)

JSON 解析

Java 没有内建的 JSON 解析函数,客户端使用 Jackson JSON 库。

Go 在标准库中有 JSON 的支持,但它没有提供足够多的钩子函数来展现 JSON 解析的过程。

我并没有尝试匹配所有的 Java 功能,因为 Go 内置的 JSON 支持看起来已经足够灵活。

Go 代码更短

简短不是 Java 的属性,而是写出符合语言习惯代码的文化的属性。

在 Java 中,setter 和 getter 方法很常见。比如,Java 代码:

class Foo {
    private int bar;

    public void setBar(int bar) {
        this.bar = bar;
    }

    public int getBar() {
        return this.bar;
    }
}

Go 语言版本如下:

type Foo struct {
    Bar int
}

3 行 vs 11 行。当你有大量的类,类内有很多成员时,这么做可以不断累加这些类。

大部分其他的代码最后长度基本差不多。

使用 Notion 来组织工作

我是 Notion.so 的重度用户。用最简单的话来说,Notion 是一个多级笔记记录应用。可以把它看做是 Evernote 和 wiki 的结合,是由顶级软件设计师精心设计和实现的。

下面是我使用 Notion 组织 Go 移植工作的方式:

下面是具体的内容:

  • 我有一个没有在上面展示的带日历视图的页面,用来记录在特定时间的工作内容和花费时间的简短笔记。因为这次合约是按小时收费,所以工作时长的统计是很重要的信息。感谢这些笔记,我知道我在 11 个月里在这次开发上花费了 601 个小时。

  • 客户喜欢了解进展。我有一个页面,记录了每月的工作总结,如下所示:

这些页面与客户共享。

  • 当开始每天的工作时,短期的 todo list 很有用。

  • 我甚至用 Notion 页面管理发票,使用“导出为 PDF”功能来生成发票的 PDF 版本。

待招聘的 Go 程序员

你的公司还需要 Go 开发者吗?你可以雇用我

额外的资源

针对问题,我提供了一些额外的说明:

  • Hacker News discussion

  • /r/golang discussion

其他资料:

  • 如果你需要一个 NoSQL,JSON 文档数据库,可以试一下 RavenDB。它拥有完备的高级特性。

  • 如果你使用 Go 编程,可以免费阅读 Essential Go 这本编程书籍。

  • 如果你对 Notion 感兴趣,我是 Notion 世界级的高级用户:

    • 我逆向了 Notion API

    • 我写了一个 Notion API 的非官方的 Go 库

    • 本网站的所有内容都是使用 Notion 编写,并使用我定制化的工具链发布。

相关 [万行 java 代码] 推荐:

[译] 将 5 万行 Java 代码移植到 Go 学到的经验

- - IT瘾-tuicool
原文作者:Krzysztof Kowalczyk. 译文出处: https://blog.kowalczyk.info. 我曾经签订了一个把大型的 Java 代码库迁移至 Go 的工作合同. 这份代码是 RavenDB 这一 NoSQL JSON 文档数据库的 Java 客户端. 包含测试代码,一共有约 5 万行.

Java代码优化

- - ImportNew
2016年3月修改,结合自己的工作和平时学习的体验重新谈一下为什么要进行代码优化. 在修改之前,我的说法是这样的:. 就像鲸鱼吃虾米一样,也许吃一个两个虾米对于鲸鱼来说作用不大,但是吃的虾米多了,鲸鱼自然饱了. 代码优化一样,也许一个两个的优化,对于提升代码的运行效率意义不大,但是只要处处都能注意代码优化,总体来说对于提升代码的运行效率就很有用了.

java代码开发规范

- - BlogJava_首页
格式规范:                                                                      .       1、TAB空格的数量. 编辑器上的TAB空格数量统一取值为4.       2、换行, 每行120字符.       3、if语句的嵌套层数3层以内   .

Java 代码性能优化

- - IT瘾-geek
代码 优化,一个很重要的课题. 可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢. 这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗. 没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了. 代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的.

2011年度最牛逼java代码

- sunseesiu - ITeye论坛最新讨论
作者: 5172306 . 声明: 本文系ITeye网站发布的原创文章,未经作者书面许可,严禁任何网站转载本文,否则必将追究法律责任. 已有 16 人发表回复,猛击->>这里<<-参与讨论. —软件人才免语言低担保 赴美带薪读研.

Java代码编写的30条建议

- - inJava
(1) 类名首字母应该大写. 字段、方法以及对象(句柄)的首字母应小写. 对于所有标识符,其中包含的所有单词都应紧靠在一起,而且大写中间单词的首字母. 若在定义中出现了常数初始化字符,则大写static final基本类型标识符中的所有字母. 这样便可标志出它们属于编译期的常数. Java包(Package)属于一种特殊情况:它们全都是小写字母,即便中间的单词亦是如此.

编写高质量的Java代码

- - 研发管理 - ITeye博客
Java 开发通用方法和准则. 不要在变量和常量出现易混淆字母:int i=1l;. 三元操作符的值类型务必保持一致;. 避免带有变长参数的方法重载:Java 5 引入了变长参数,varags, 用…表示,变长参数必须是最后一个参数,一个方法不能有多个变长参数;. 别让null和空值影响到变长方法,主要出现在在变长方法的重载;.

如何写出更好的Java代码

- - Java译站
Java是最流行的编程语言之一,但似乎并没有人喜欢使用它. 好吧,实际上Java是一门还不错的编程语言,由于最近Java 8发布了,我决定来编辑一个如何能更好地使用Java的列表,这里面包括一些库,实践技巧以及工具. 这篇文章在 GitHub上也有. 你可以随时在上面贡献或者添加你自己的Java使用技巧或者最佳实践.

Matlab中调用第三方Java代码

- - ITeye博客
在Java中采用Matlab JA Builder可以实现调用m文件,采用这样的方式,可在Matlab的M文件中,直接调用Java类. 这种方式可以表示为Java--> Matlab( m, Java),即整个程序由Java启动. 那如何使用Matlab直接调用Java的方式. 错误的方法:网上有些说将jar、class放在Matlab的Search路径中.

JAVA代码编写的30条建议

- - Java - 编程语言 - ITeye博客
今天和大家谈论到的是关于Java代码编码方面的一些建议. 这里整理到JAVA代码编写的30条建议:. 字段、方法以及对象(句柄)的首字母应小写. 对于所有标识符,其中包含的所有单词都应紧靠在一起,而且大写中间单词的首字母. 若在定义中出现了常数初始化字符,则大写static final基本类型标识符中的所有字母.