Golang 中的良好代碼與糟糕代碼

最近,有人要求我詳細解釋在 Golang 中什么是好的代碼和壞的代碼。我覺得這個練習非常有趣。實際上,足夠有趣以至于我寫了一篇關于這個話題的文章。為了說明我的回答,我選擇了我在空中交通管理(ATM)領域遇到的一個具體用例。

img

背景

首先,簡要解釋一下實現的背景。

歐洲航空管制組織(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()。

逐步比較

現在,讓我們逐步看看我認為是糟糕代碼的部分,以及我是如何重構它的。

字符串 vs []byte

糟糕的實現僅處理字符串輸入。由于 Go 提供了對字節操作的強大支持(基本操作如修剪、正則表達式等),并且考慮到輸入很可能是 []byte(考慮到 AFTN 消息是通過 TCP 接收的),實際上沒有理由強制使用字符串輸入。

錯誤處理

糟糕的實現中的錯誤處理有些糟糕。
我們可以找到一些潛在錯誤返回的情況,而第二個參數中的錯誤甚至沒有被處理:

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")
}

避免嵌套

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 <- "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 linesif len(in) == 0 || startWith(in, bytesComment) {ch <- nilreturn}token, value := parseLine(in)if token == nil {ch <- nillog.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 中使用互斥鎖可能是代碼存在問題的跡象。

  • 缺點 #2:偽共享

跨線程/協程共享內存并不是一個好主意,因為可能存在偽共享(一個 CPU 核心緩存中的緩存行可能會被另一個 CPU 核心緩存無效)。這意味著,如果線程/協程意圖對其進行更改,我們應該盡量避免在線程/協程之間共享相同的變量。

在這個例子中,我認為偽共享影響不大,因為輸入文件相當輕量級(在 Message 結構中添加填充字段并進行性能測試得到的結果大致相同)。然而,在我看來,這始終是一件需要牢記的重要事情。

現在讓我們看一下好的實現是如何處理并行處理的:

for _, line := range in {go mapLine(line, ch)
}

現在,mapLine() 只接收兩個輸入:

  • 當前行
  • 一個通道。這次,這個通道不僅用于在行處理完成時發送通知,還用于發送實際結果。這意味著不應該由 goroutine 來修改最終的 Message 結構。

父 goroutine(生成單獨的 goroutine 中的 mapLine() 調用的那個)通過以下方式收集結果:

msg := Message{}for range in {data := <-chswitch data.(type) {// Modify msg variable}
}

這個實現更符合 Go 的原則,只通過通信來共享內存。Message 變量由單個 Goroutine 修改,以防止潛在的并發切片修改和錯誤共享。

即使是好的代碼也可能面臨一個潛在的批評,就是為每一行代碼都創建一個 Goroutine。這樣的實現可以工作,因為 ADEXP 消息不會包含成千上萬行的內容。然而,在非常高的吞吐量下,簡單的實現每個請求觸發一個 Goroutine 的方式并不具有很強的可擴展性。更好的選擇可能是創建一個可重用 Goroutine 池。

編輯: 假設(一行代碼 = 一個 Goroutine)絕對不是一個好主意,因為它會導致過多的上下文切換。要獲取更多信息,請查看 further reading 章節末尾的鏈接。

處理行的通知

在不好的實現中,如上所述,一旦通過 mapLine() 完成行處理,我們應該通知父 Goroutine。這是通過使用 chan string 通道和調用來實現的:

ch <- "ok"

對于父 Goroutine 實際上并不檢查通道發送的值,更好的選擇是使用 chan struct{},使用 ch <- struct{}{},甚至更好(對 GC 更友好)的選擇是使用 chan interface{},使用 ch <- nil

另一種方法(在我看來更清晰的方法)是使用 sync.WaitGroup,因為父 Goroutine 只需在每個 mapLine() 完成后繼續執行。

If

Go 語言的 if 語句允許在條件之前傳遞一個語句。

對于這段代碼的改進版本:

f, contains := factory[string(token)]
if contains {// Do something
}

以下實現可以是這樣的:

if f, contains := factory[sToken]; contains {// Do something
}

它稍微提高了代碼的可讀性。

Switch

另一個糟糕實現的錯誤是在以下開關語句中忘記了默認情況:

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)
}

處理默認情況有助于在開發過程中盡快捕獲開發人員可能產生的潛在錯誤。

遞歸

parseComplexLines() 是一個解析復雜標記的函數。糟糕代碼中的算法是使用遞歸完成的:

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]stringcurrentMap := 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}
}

第二段代碼將比第一段代碼更高效。

常量管理

我們必須管理一個常量值以區分 ADEXP 和 ICAO 消息。糟糕的代碼是這樣做的:

const (AdexpType = 0 // TODO constantIcaoType  = 1
)

而良好的代碼是基于 Go(優雅的)iota 的更優雅的解決方案:

const (AdexpType = iotaIcaoType 
)

它產生完全相同的結果,但減少了潛在的開發人員錯誤。

接收器函數

每個解析器提供一個函數來確定消息是否涉及更高級別(至少有一個路由點在 350 級以上)。

糟糕的代碼是這樣實現的:

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

日志記錄

另一個顯而易見的例子是糟糕代碼中缺乏生成的日志。因為我不是標準日志包的粉絲,所以在這個項目中我使用了一個名為 logrus 的外部庫。

go fmt

Go 提供了一套強大的工具,比如 go fmt。不幸的是,我們忘記在糟糕的代碼上應用它,而在良好的代碼上已經做了。

DDD

領域驅動設計(DDD)引入了普遍語言的概念,強調了在整個項目參與者(業務專家、開發人員、測試人員等)之間使用共享語言的重要性。在這個例子中無法真正衡量這一點,但保持像 Message 這樣的簡單結構符合領域邊界內部使用的語言也是提高整體項目可維護性的一個好方法。

性能結果

在 i7–7700 4x 3.60Ghz 上,我進行了基準測試來比較兩個解析器:

  • 糟糕的實現:60430 納秒/操作
  • 良好的實現:45996 納秒/操作

糟糕的代碼比良好的代碼慢了超過30%。

結論

在我看來,很難給出糟糕代碼和良好代碼的一般定義。在一個上下文中的代碼可能被認為是好的,而在另一個上下文中可能被認為是糟糕的。

良好代碼的第一個明顯特征是根據給定的功能需求提供正確的解決方案。如果代碼不符合需求,即使它很高效,也是相當無用的。

同時,對于開發人員來說,關心簡單、易維護和高效的代碼也很重要。

性能改進并非憑空而來,它伴隨著代碼復雜性的增加。

一個優秀的開發人員是能夠在特定的上下文中找到這些特性之間的平衡的人。

就像在 DDD 中一樣,上下文是關鍵的 😃

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/160519.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/160519.shtml
英文地址,請注明出處:http://en.pswp.cn/news/160519.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

linux部署jar 常見問題

1.java -jar xxx.jar no main manifest attribute, in xxx.jar 一.no main manifest attribute, in xxx.jar 在pom.xml文件中加入&#xff1a; <plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifac…

C語言每日一題(35)有效的括號

力扣網 20 有效的括號 題目描述 給定一個只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判斷字符串是否有效。 有效字符串需滿足&#xff1a; 左括號必須用相同類型的右括號閉合。左括號必須以正確的順序閉合。每個右…

CountDownLatch和CyclicBarrier

JUC&#xff08;Java.util.concurrent&#xff09;是Java 5中引入的一個并發編程庫&#xff0c;它包含了許多用于多線程處理的工具類和接口。JUC主要提供了以下特性&#xff1a; 線程池&#xff1a;線程池可以提高線程的使用效率&#xff0c;避免頻繁地創建和銷毀線程&#xff…

Kotlin學習——hello kotlin 函數function 變量 類 + 泛型 + 繼承

Kotlin 是一門現代但已成熟的編程語言&#xff0c;旨在讓開發人員更幸福快樂。 它簡潔、安全、可與 Java 及其他語言互操作&#xff0c;并提供了多種方式在多個平臺間復用代碼&#xff0c;以實現高效編程。 https://play.kotlinlang.org/byExample/01_introduction/02_Functio…

Docker Swarm總結(2/3)

目錄 8、service 操作 8.1 task 伸縮 8.2 task 容錯 8.3 服務刪除 8.4 滾動更新 8.5 更新回滾 9、service 全局部署模式 9.1 環境變更 9.2 創建 service 9.3 task 伸縮 10、overlay 網絡 10.1 測試環境 1搭建 10.2 overlay 網絡概述 10.3 docker_gwbridg 網絡基礎…

【DevOps】Git 圖文詳解(八):后悔藥 - 撤銷變更

Git 圖文詳解&#xff08;八&#xff09;&#xff1a;后悔藥 - 撤銷變更 1.后悔指令 &#x1f525;2.回退版本 reset3.撤銷提交 revert4.checkout / reset / revert 總結 發現寫錯了要回退怎么辦&#xff1f;看看下面幾種后悔指令吧&#xff01; ? 還沒提交的怎么撤銷&#x…

Visual Studio連接unity編輯器_unity基礎開發教程

Visual Studio連接unity編輯器 問題描述解決方法意外情況 問題描述 當我們在unity編輯器中打開C#腳本的時候發現Visual Studio沒有連接unity編輯器&#xff0c;在編寫代碼的時候也沒有unity關鍵字的提醒。 簡單來說就是敲代碼沒有代碼提示。 解決方法 這時候需要在unity中進行…

Qt實現圖片旋轉的幾種方式(全)

目錄 一、用手搓&#xff08;QPainter&#xff09; 二、使用 QGraphicsView 和 QGraphicsPixmapItem 三、使用 QTransform 實現圖像旋轉 四、利用 OpenGL 實現旋轉圖像的效果有幾種不同的方法&#xff0c;其中常見的包括&#xff1a; 手動旋轉繪制&#xff1a; 使用 QPaint…

網絡吞吐量 公網帶寬有關嗎?

環境&#xff1a; 華為交換機 深信服防火墻 問題描述&#xff1a; 網絡吞吐量 公網帶寬有關嗎&#xff1f; 解決方案&#xff1a; 網絡吞吐量網絡吞吐量是指在特定時間內通過網絡傳輸的數據量。它衡量了網絡設備&#xff08;如防火墻、交換機、路由器&#xff09;或網絡連…

終端仿真軟件 SecureCRT v9.4.2

SecureCRT是一款終端仿真軟件&#xff0c;它提供了類似于Telnet和SSH等協議的遠程訪問功能。SecureCRT專門為網絡管理員、系統管理員和其他需要保密訪問網絡設備的用戶設計。 SecureCRT具有以下特點&#xff1a; 安全性&#xff1a;SecureCRT支持SSH1、SSH2、SSL和TLS等加密和…

素短語的定義

素短語&#xff0c;是指至少含有一個終結符的短語&#xff0c;并且除自身外&#xff0c;不包含更小的素短語。 最左素短語是句型中最左邊的素短語。

7.HTML中列表標簽

7.列表標簽 7.1無序列表&#xff08;重點&#xff09; 表格是用來顯示數據的&#xff0c;那么列表就是用來布局的。 列表最大的特點就是整齊&#xff0c;整潔&#xff0c;有序&#xff0c;他作為布局會更加自由和方便&#xff0c; 根據使用的情景不同&#xff0c;列表可分為三…

數字圖像處理(岡薩雷斯)學習筆記

目錄 一.機器視覺和計算機視覺二.圖像處理基礎1.什么是圖像2.如何訪問圖像 三.圖像仿射變換四.灰度變換 一.機器視覺和計算機視覺 機器視覺(Machine Vision,MV)和計算機視覺(Computer Vision&#xff0c;CV)的區別和聯系&#xff1a; 機器視覺更注重廣義圖像信號(激光&#xff…

C#中的Fody

在C#中&#xff0c;NuGet里的Fody是一個用于.NET應用程序的代碼增強工具。它通過在編譯過程中自動織入代碼&#xff0c;改變目標程序集的行為。Fody的一個常見用途是簡化屬性通知的實現&#xff0c;特別適用于WPF綁定。 在WPF中&#xff0c;屬性通知是一種機制&#xff0c;用于…

C語言操作符例題

這里寫目錄標題 例題一題目解析 例題二題目解析 例題三方法一方法二方法三 例題四例題五 感謝各位大佬對我的支持,如果我的文章對你有用,歡迎點擊以下鏈接 &#x1f412;&#x1f412;&#x1f412; 個人主頁 &#x1f978;&#x1f978;&#x1f978; C語言 &#x1f43f;?…

智能指針(Newbie Note)

智能指針專題 1.普通指針的問題2.智能指針是什么什么是所有權 3.智能指針三個好處&#xff1a;4.C11提供的智能指針4.1 shared_ptr&#xff08;共享所有權指針&#xff09;4.1.1 分配內存4.1.2 成員函數4.1.3 計數情況匯總&#xff1a;4.1.4 示例代碼(計數)4.1.5 示例代碼(rese…

Java深拷貝與淺拷貝技術解析及實例演示

摘要&#xff1a;本文將詳細介紹Java中的深拷貝和淺拷貝概念&#xff0c;通過分析源碼和舉例說明&#xff0c;幫助讀者更好地理解這兩種拷貝方式的區別及應用場景。 一、深拷貝與淺拷貝的概念 深拷貝&#xff1a;復制一個對象后&#xff0c;無論是基本數據類型還是引用類型&…

多柱漢諾塔問題

k柱漢諾塔 題目描述 漢諾塔&#xff08;Hanoi Tower&#xff09;&#xff0c;又稱河內塔。 傳說大梵天創造世界的時候做了三根金剛石柱子&#xff0c;按左、中、右排序。大梵天在左側的柱子上&#xff0c;從下往上按照大小順序摞著64片黃金圓盤&#xff0c;越靠下的圓盤越大。…

個人博客項目 - 測試報告

文章目錄 一、項目背景二、測試報告功能測試1.編寫測試用例2.登錄測試3.編寫文章測試4.查看文章測試5.刪除文章測試7.注銷登錄測試 自動化測試性能測試1.VUG2.進行場景設計3.生成性能測試報告 總結 本文開始 一、項目背景 通過學習測試相關的知識&#xff0c;動手實踐并測試一…

2023 年 亞太賽 APMCM ABC題 國際大學生數學建模挑戰賽 |數學建模完整代碼+建模過程全解全析

當大家面臨著復雜的數學建模問題時&#xff0c;你是否曾經感到茫然無措&#xff1f;作為2022年美國大學生數學建模比賽的O獎得主&#xff0c;我為大家提供了一套優秀的解題思路&#xff0c;讓你輕松應對各種難題。 以五一杯 A題為例子&#xff0c;以下是咱們做的一些想法呀&am…