首先,简要解释一下实现的背景。欧洲航空管制组织(Eurocontrol)是管理欧洲各国航空交通的组织。Eurocontrol 与航空导航服务提供商(ANSP)之间交换数据的通用网络称为 AFTN。这个网络主要用于交换两种不同类型的消息:ADEXP 和 ICAO 消息。每种消息类型都有自己的语法,但在语义上,这两种类型是等价的(或多或少)。在这个上下文中,性能 必须是实现的关键要素。
preprocessed, _ := preprocess(string)优秀的实现处理了每一个可能的错误:
preprocessed, err := preprocess(bytes) if err != nil { return Message{}, err }我们还可以在糟糕的实现中找到一些错误,就像下面的代码中所示:
if len(in) == 0 { return "", fmt.Errorf("Input is empty") }第一个错误是语法错误。根据 Go 的规范,错误字符串既不应该大写,也不应该以标点结束。
if len(in) == 0 { return nil, errors.New("input is empty") }
// 堆代码 duidaima.com 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 <- "ok" } else { data := f(token, value) enrichMessage(msg, data) ch <- "ok" } } else { ch <- "ok" return } } else { ch <- "ok" return } }相反,优秀的实现是一个扁平的表示方式:
func mapLine(in []byte, ch chan interface{}) { // Filter empty lines and comment lines if len(in) == 0 || startWith(in, bytesComment) { ch <- nil return } token, value := parseLine(in) if token == nil { ch <- nil log.Warnf("Token name is empty on line %v", string(in)) return } sToken := string(token) if f, contains := factory[sToken]; contains { ch <- f(sToken, value) return } log.Warnf("Token %v is not managed by the parser", string(in)) ch <- nil }这样做在我看来使代码更易读。此外,这种扁平的表示方式也必须应用到错误管理中。举个例子:
a, err := f1() if err == nil { b, err := f2() if err == nil { return b, nil } else { return nil, err } } else { return nil, err }应该被替换为:
a, err := f1() if err != nil { return nil, err } b, err := f2() if err != nil { return nil, err } return b, nil再次,第二个代码版本更容易阅读。
func preprocess(in container) (container, error) { }考虑到这个项目的背景(性能很重要),并考虑到消息可能会相当庞大,更好的选择是传递对容器结构的指针。否则,在先前的示例中,每次调用都会复制容器值。优秀的实现并不面临这个问题,因为它处理切片(无论底层数据如何,都是一个简单的 24 字节结构)。
func preprocess(in []byte) ([][]byte, error) { }糟糕的实现基于一个很好的初始想法:利用 goroutine 并行处理数据(每行一个 goroutine)。这是通过在循环遍历行数的过程中,为每一行启动一个 mapLine() 调用的 goroutine 完成的。
for i := 0; i < len(lines); i++ { go mapLine(&msg, lines[i], ch) }因为结构中包含一些切片,这些切片可能会被并发地修改(由两个或更多的 goroutine 同时修改),在糟糕的实现中,我们不得不处理互斥锁。例如,Message 结构包含一个 Estdata []estdata。 通过添加另一个 estdata 来修改切片必须这样做:
mutexEstdata.Lock() for _, v := range value { fl := extractFlightLevel(v[subtokenFl]) msg.Estdata = append(msg.Estdata, estdata{v[subtokenPtid], v[subtokenEto], fl}) } mutexEstdata.Unlock()现实情况是,除非是非常特殊的用例,必须在 goroutine 中使用互斥锁可能是代码存在问题的迹象。
for _, line := range in { go mapLine(line, ch) }现在,mapLine() 只接收两个输入:
msg := Message{} for range in { data := <-ch switch data.(type) { // Modify msg variable } }这个实现更符合 Go 的原则,只通过通信来共享内存。Message 变量由单个 Goroutine 修改,以防止潜在的并发切片修改和错误共享。
ch <- "ok"对于父 Goroutine 实际上并不检查通道发送的值,更好的选择是使用 chan struct{},使用 ch <- struct{}{},甚至更好(对 GC 更友好)的选择是使用 chan interface{},使用 ch <- nil。
f, contains := factory[string(token)] if contains { // Do something }以下实现可以是这样的:
if f, contains := factory[sToken]; contains { // Do something }它稍微提高了代码的可读性。
switch simpleToken.token { case tokenTitle: msg.Title = value case tokenAdep: msg.Adep = value case tokenAltnz: msg.Alternate = value // Other cases }如果开发者考虑了所有不同的情况,那么默认情况可以是可选的。然而,像以下示例中这样捕捉特定情况肯定更好:
switch simpleToken.token { case tokenTitle: msg.Title = value case tokenAdep: msg.Adep = value case tokenAltnz: msg.Alternate = value // Other cases default: log.Errorf("unexpected token type %v", simpleToken.token) return Message{}, fmt.Errorf("unexpected token type %v", simpleToken.token) }处理默认情况有助于在开发过程中尽快捕获开发人员可能产生的潜在错误。
func parseComplexLines(in string, currentMap map[string]string, out []map[string]string) []map[string]string { match := regexpSubfield.Find([]byte(in)) if match == nil { out = append(out, currentMap) return out } sub := string(match) h, l := parseLine(sub) _, contains := currentMap[string(h)] if contains { out = append(out, currentMap) currentMap = make(map[string]string) } currentMap[string(h)] = string(strings.Trim(l, stringEmpty)) return parseComplexLines(in[len(sub):], currentMap, out) }然而,Go 不支持尾递归消除以优化子函数调用。良好的代码产生完全相同的结果,但使用迭代算法:
func parseComplexToken(token string, value []byte) interface{} { if value == nil { log.Warnf("Empty value") return complexToken{token, nil} } var v []map[string]string currentMap := make(map[string]string) matches := regexpSubfield.FindAll(value, -1) for _, sub := range matches { h, l := parseLine(sub) if _, contains := currentMap[string(h)]; contains { v = append(v, currentMap) currentMap = make(map[string]string) } currentMap[string(h)] = string(bytes.Trim(l, stringEmpty)) } v = append(v, currentMap) return complexToken{token, v} }第二段代码将比第一段代码更高效。
const ( AdexpType = 0 // TODO constant IcaoType = 1 )而良好的代码是基于 Go(优雅的)iota 的更优雅的解决方案:
const ( AdexpType = iota IcaoType )它产生完全相同的结果,但减少了潜在的开发人员错误。
func IsUpperLevel(m Message) bool { for _, r := range m.RoutePoints { if r.FlightLevel > upperLevel { return true } } return false }意味着我们必须将消息作为函数的输入参数传递。 而良好的代码只是一个带有消息接收器的函数:
func (m *Message) IsUpperLevel() bool { for _, r := range m.RoutePoints { if r.FlightLevel > upperLevel { return true } } return false }第二种方法更可取。我们只需指示消息结构实现了特定的行为。这也可能是使用 Go 接口的第一步。例如,如果将来我们需要创建另一个具有相同行为(IsUpperLevel())的结构体,初始代码甚至不需要重构(因为消息已经实现了这个行为)。
// Split each line in a goroutine for _, line := range in { go mapLine(line, ch) } msg := Message{} // Gather the goroutine results for range in { // ... }除了函数注释之外,一个具体的例子也可能非常有用:
// Parse a line by returning the header (token name) and the value. // Example: -COMMENT TEST must returns COMMENT and TEST (in byte slices) func parseLine(in []byte) ([]byte, []byte) { // ... }这样具体的例子可以帮助其他开发人员更好地理解现有项目。最后但同样重要的是,根据 Go 的最佳实践,包本身也应进行注释。
/* Package good is a library for parsing the ADEXP messages. An intermediate format Message is built by the parser. */ package good