最近,有人要求我详细解释在 Golang 中什么是好的代码和坏的代码。我觉得这个练习非常有趣。实际上,足够有趣以至于我写了一篇关于这个话题的文章。为了说明我的回答,我选择了我在空中交通管理(ATM)领域遇到的一个具体用例。
一、背景
首先,简要解释一下实现的背景。
欧洲航空管制组织(Eurocontrol)是管理欧洲各国航空交通的组织。Eurocontrol 与航空导航服务提供商(ANSP)之间交换数据的通用网络称为 AFTN。这个网络主要用于交换两种不同类型的消息:ADEXP 和 ICAO 消息。每种消息类型都有自己的语法,但在语义上,这两种类型是等价的(或多或少)。在这个上下文中,性能 必须是实现的关键要素。
该项目需要提供两种基于 Go 解析 ADEXP 消息的实现(ICAO 没有在这个练习中处理):
- 一个糟糕的实现(包名:bad)
- 一个重构后的实现(包名:good)
可以在 这里 找到 ADEXP 消息的示例。
在这个练习中,解析器只处理了 ADEXP 消息中的一部分字段。但这仍然是相关的,因为它可以说明常见的 Golang 错误。
二、解析
简而言之,ADEXP 消息是一组令牌。令牌类型可以是:一组令牌的重复列表。每行包含一组令牌子列表(在本示例中为 GEOID、LATTD、LONGTD)。
考虑到这个背景,重要的是要实现一个可以利用并行性的版本。所以算法如下:
- 预处理步骤来清理和重新排列输入消息(我们必须清除潜在的空格,重新排列多行的令牌,如 COMMENT 等)。
- 然后在一个给定的 goroutine 中拆分每一行。每个 goroutine 将负责处理一行并返回结果。
- 最后,收集结果并返回一个 Message 结构。这个结构是一个通用的结构,无论消息类型是 ADEXP 还是 ICAO。
每个包都包含一个 adexp.go 文件,暴露了主要的函数 ParseAdexpMessage()。
三、逐步比较
现在,让我们逐步看看我认为是糟糕代码的部分,以及我是如何重构它的。
1.字符串 vs []byte
糟糕的实现仅处理字符串输入。由于 Go 提供了对字节操作的强大支持(基本操作如修剪、正则表达式等),并且考虑到输入很可能是 []byte(考虑到 AFTN 消息是通过 TCP 接收的),实际上没有理由强制使用字符串输入。
2.错误处理
糟糕的实现中的错误处理有些糟糕。 我们可以找到一些潜在错误返回的情况,而第二个参数中的错误甚至没有被处理:
preprocessed, _ := preprocess(string)
优秀的实现处理了每一个可能的错误:
preprocessed, err := preprocess(bytes)
if err != nil {
return Message{}, err
}
我们还可以在糟糕的实现中找到一些错误,就像下面的代码中所示:
if len(in) == 0 {
return "", fmt.Errorf("Input is empty")
}
第一个错误是语法错误。根据 Go 的规范,错误字符串既不应该大写,也不应该以标点结束。
第二个错误是因为如果一个错误字符串是一个简单的常量(不需要格式化),使用 errors.New() 更为高效。
优秀的实现看起来是这样的:
if len(in) == 0 {
return nil, errors.New("input is empty")
}
3.避免嵌套
mapLine() 函数是一个避免嵌套调用的良好示例。糟糕的实现:
func mapLine(msg *Message, in string, ch chan string) {
if !startWith(in, stringComment) {
token, value := parseLine(in)
if token != "" {
f, contains := factory[string(token)]
if !contains {
ch