成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

開(kāi)發(fā) 后端
良好代碼的第一個(gè)明顯特征是根據(jù)給定的功能需求提供正確的解決方案。如果代碼不符合需求,即使它很高效,也是相當(dāng)無(wú)用的。

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

一、背景

首先,簡(jiǎn)要解釋一下實(shí)現(xiàn)的背景。

歐洲航空管制組織(Eurocontrol)是管理歐洲各國(guó)航空交通的組織。Eurocontrol 與航空導(dǎo)航服務(wù)提供商(ANSP)之間交換數(shù)據(jù)的通用網(wǎng)絡(luò)稱為 AFTN。這個(gè)網(wǎng)絡(luò)主要用于交換兩種不同類型的消息:ADEXP 和 ICAO 消息。每種消息類型都有自己的語(yǔ)法,但在語(yǔ)義上,這兩種類型是等價(jià)的(或多或少)。在這個(gè)上下文中,性能 必須是實(shí)現(xiàn)的關(guān)鍵要素。

該項(xiàng)目需要提供兩種基于 Go 解析 ADEXP 消息的實(shí)現(xiàn)(ICAO 沒(méi)有在這個(gè)練習(xí)中處理):

  • 一個(gè)糟糕的實(shí)現(xiàn)(包名:bad)
  • 一個(gè)重構(gòu)后的實(shí)現(xiàn)(包名:good)

可以在 這里 找到 ADEXP 消息的示例。

在這個(gè)練習(xí)中,解析器只處理了 ADEXP 消息中的一部分字段。但這仍然是相關(guān)的,因?yàn)樗梢哉f(shuō)明常見(jiàn)的 Golang 錯(cuò)誤。

二、解析

簡(jiǎn)而言之,ADEXP 消息是一組令牌。令牌類型可以是:一組令牌的重復(fù)列表。每行包含一組令牌子列表(在本示例中為 GEOID、LATTD、LONGTD)。

考慮到這個(gè)背景,重要的是要實(shí)現(xiàn)一個(gè)可以利用并行性的版本。所以算法如下:

  • 預(yù)處理步驟來(lái)清理和重新排列輸入消息(我們必須清除潛在的空格,重新排列多行的令牌,如 COMMENT 等)。
  • 然后在一個(gè)給定的 goroutine 中拆分每一行。每個(gè) goroutine 將負(fù)責(zé)處理一行并返回結(jié)果。
  • 最后,收集結(jié)果并返回一個(gè) Message 結(jié)構(gòu)。這個(gè)結(jié)構(gòu)是一個(gè)通用的結(jié)構(gòu),無(wú)論消息類型是 ADEXP 還是 ICAO。

每個(gè)包都包含一個(gè) adexp.go 文件,暴露了主要的函數(shù) ParseAdexpMessage()。

三、逐步比較

現(xiàn)在,讓我們逐步看看我認(rèn)為是糟糕代碼的部分,以及我是如何重構(gòu)它的。

1.字符串 vs []byte

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

2.錯(cuò)誤處理

糟糕的實(shí)現(xiàn)中的錯(cuò)誤處理有些糟糕。 我們可以找到一些潛在錯(cuò)誤返回的情況,而第二個(gè)參數(shù)中的錯(cuò)誤甚至沒(méi)有被處理:

preprocessed, _ := preprocess(string)

優(yōu)秀的實(shí)現(xiàn)處理了每一個(gè)可能的錯(cuò)誤:

preprocessed, err := preprocess(bytes)
if err != nil {
  return Message{}, err
}

我們還可以在糟糕的實(shí)現(xiàn)中找到一些錯(cuò)誤,就像下面的代碼中所示:

if len(in) == 0 {
  return "", fmt.Errorf("Input is empty")
}

第一個(gè)錯(cuò)誤是語(yǔ)法錯(cuò)誤。根據(jù) Go 的規(guī)范,錯(cuò)誤字符串既不應(yīng)該大寫,也不應(yīng)該以標(biāo)點(diǎn)結(jié)束。

第二個(gè)錯(cuò)誤是因?yàn)槿绻粋€(gè)錯(cuò)誤字符串是一個(gè)簡(jiǎn)單的常量(不需要格式化),使用 errors.New() 更為高效。

優(yōu)秀的實(shí)現(xiàn)看起來(lái)是這樣的:

if len(in) == 0 {
    return nil, errors.New("input is empty")
}

3.避免嵌套

mapLine() 函數(shù)是一個(gè)避免嵌套調(diào)用的良好示例。糟糕的實(shí)現(xiàn):

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

相反,優(yōu)秀的實(shí)現(xiàn)是一個(gè)扁平的表示方式:

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
}

這樣做在我看來(lái)使代碼更易讀。此外,這種扁平的表示方式也必須應(yīng)用到錯(cuò)誤管理中。舉個(gè)例子:

a, err := f1()
if err == nil {
    b, err := f2()
    if err == nil {
        return b, nil
    } else {
        return nil, err
    }
} else {
    return nil, err
}

應(yīng)該被替換為:

a, err := f1()
if err != nil {
    return nil, err
}
b, err := f2()
if err != nil {
    return nil, err
}
return b, nil

再次,第二個(gè)代碼版本更容易閱讀。

4.傳遞數(shù)據(jù)是按引用還是按值傳遞

在糟糕的實(shí)現(xiàn)中,預(yù)處理函數(shù)的簽名是:

func preprocess(in container) (container, error) {
}

考慮到這個(gè)項(xiàng)目的背景(性能很重要),并考慮到消息可能會(huì)相當(dāng)龐大,更好的選擇是傳遞對(duì)容器結(jié)構(gòu)的指針。否則,在先前的示例中,每次調(diào)用都會(huì)復(fù)制容器值。

優(yōu)秀的實(shí)現(xiàn)并不面臨這個(gè)問(wèn)題,因?yàn)樗幚砬衅o(wú)論底層數(shù)據(jù)如何,都是一個(gè)簡(jiǎn)單的 24 字節(jié)結(jié)構(gòu))。

func preprocess(in []byte) ([][]byte, error) {
}

糟糕的實(shí)現(xiàn)基于一個(gè)很好的初始想法:利用 goroutine 并行處理數(shù)據(jù)(每行一個(gè) goroutine)。

這是通過(guò)在循環(huán)遍歷行數(shù)的過(guò)程中,為每一行啟動(dòng)一個(gè) mapLine() 調(diào)用的 goroutine 完成的。

for i := 0; i < len(lines); i++ {
    go mapLine(&msg, lines[i], ch)
}

因?yàn)榻Y(jié)構(gòu)中包含一些切片,這些切片可能會(huì)被并發(fā)地修改(由兩個(gè)或更多的 goroutine 同時(shí)修改),在糟糕的實(shí)現(xiàn)中,我們不得不處理互斥鎖。

例如,Message 結(jié)構(gòu)包含一個(gè) Estdata []estdata。 通過(guò)添加另一個(gè) estdata 來(lái)修改切片必須這樣做:

mutexEstdata.Lock()
for _, v := range value {
    fl := extractFlightLevel(v[subtokenFl])
    msg.Estdata = append(msg.Estdata, estdata{v[subtokenPtid], v[subtokenEto], fl})
}
mutexEstdata.Unlock()

現(xiàn)實(shí)情況是,除非是非常特殊的用例,必須在 goroutine 中使用互斥鎖可能是代碼存在問(wèn)題的跡象。

5.缺點(diǎn) #2:偽共享

跨線程/協(xié)程共享內(nèi)存并不是一個(gè)好主意,因?yàn)榭赡艽嬖趥喂蚕恚ㄒ粋€(gè) CPU 核心緩存中的緩存行可能會(huì)被另一個(gè) CPU 核心緩存無(wú)效)。這意味著,如果線程/協(xié)程意圖對(duì)其進(jìn)行更改,我們應(yīng)該盡量避免在線程/協(xié)程之間共享相同的變量。

在這個(gè)例子中,我認(rèn)為偽共享影響不大,因?yàn)檩斎胛募喈?dāng)輕量級(jí)(在 Message 結(jié)構(gòu)中添加填充字段并進(jìn)行性能測(cè)試得到的結(jié)果大致相同)。然而,在我看來(lái),這始終是一件需要牢記的重要事情。

現(xiàn)在讓我們看一下好的實(shí)現(xiàn)是如何處理并行處理的:

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

現(xiàn)在,mapLine() 只接收兩個(gè)輸入:

  • 當(dāng)前行
  • 一個(gè)通道。這次,這個(gè)通道不僅用于在行處理完成時(shí)發(fā)送通知,還用于發(fā)送實(shí)際結(jié)果。這意味著不應(yīng)該由 goroutine 來(lái)修改最終的 Message 結(jié)構(gòu)。

父 goroutine(生成單獨(dú)的 goroutine 中的 mapLine() 調(diào)用的那個(gè))通過(guò)以下方式收集結(jié)果:

msg := Message{}

for range in {
    data := <-ch

    switch data.(type) {
        // Modify msg variable
    }
}

這個(gè)實(shí)現(xiàn)更符合 Go 的原則,只通過(guò)通信來(lái)共享內(nèi)存。Message 變量由單個(gè) Goroutine 修改,以防止?jié)撛诘牟l(fā)切片修改和錯(cuò)誤共享。

即使是好的代碼也可能面臨一個(gè)潛在的批評(píng),就是為每一行代碼都創(chuàng)建一個(gè) Goroutine。這樣的實(shí)現(xiàn)可以工作,因?yàn)?ADEXP 消息不會(huì)包含成千上萬(wàn)行的內(nèi)容。然而,在非常高的吞吐量下,簡(jiǎn)單的實(shí)現(xiàn)每個(gè)請(qǐng)求觸發(fā)一個(gè) Goroutine 的方式并不具有很強(qiáng)的可擴(kuò)展性。更好的選擇可能是創(chuàng)建一個(gè)可重用 Goroutine 池。

編輯: 假設(shè)(一行代碼 = 一個(gè) Goroutine)絕對(duì)不是一個(gè)好主意,因?yàn)樗鼤?huì)導(dǎo)致過(guò)多的上下文切換。要獲取更多信息,請(qǐng)查看 further reading 章節(jié)末尾的鏈接。

6.處理行的通知

在不好的實(shí)現(xiàn)中,如上所述,一旦通過(guò) mapLine() 完成行處理,我們應(yīng)該通知父 Goroutine。這是通過(guò)使用 chan string 通道和調(diào)用來(lái)實(shí)現(xiàn)的:

ch <- "ok"

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

另一種方法(在我看來(lái)更清晰的方法)是使用 sync.WaitGroup,因?yàn)楦?Goroutine 只需在每個(gè) mapLine() 完成后繼續(xù)執(zhí)行。

7.If

Go 語(yǔ)言的 if 語(yǔ)句允許在條件之前傳遞一個(gè)語(yǔ)句。

對(duì)于這段代碼的改進(jìn)版本:

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

以下實(shí)現(xiàn)可以是這樣的:

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

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

8.Switch

另一個(gè)糟糕實(shí)現(xiàn)的錯(cuò)誤是在以下開(kāi)關(guān)語(yǔ)句中忘記了默認(rèn)情況:

switch simpleToken.token {
case tokenTitle:
    msg.Title = value
case tokenAdep:
    msg.Adep = value
case tokenAltnz:
    msg.Alternate = value 
// Other cases
}

如果開(kāi)發(fā)者考慮了所有不同的情況,那么默認(rèn)情況可以是可選的。然而,像以下示例中這樣捕捉特定情況肯定更好:

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

處理默認(rèn)情況有助于在開(kāi)發(fā)過(guò)程中盡快捕獲開(kāi)發(fā)人員可能產(chǎn)生的潛在錯(cuò)誤。

9.遞歸

parseComplexLines() 是一個(gè)解析復(fù)雜標(biāo)記的函數(shù)。糟糕代碼中的算法是使用遞歸完成的:

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 不支持尾遞歸消除以優(yōu)化子函數(shù)調(diào)用。良好的代碼產(chǎn)生完全相同的結(jié)果,但使用迭代算法:

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

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

10.常量管理

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

const (
    AdexpType = 0 // TODO constant
    IcaoType  = 1
)

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

const (
    AdexpType = iota
    IcaoType 
)

它產(chǎn)生完全相同的結(jié)果,但減少了潛在的開(kāi)發(fā)人員錯(cuò)誤。

11.接收器函數(shù)

每個(gè)解析器提供一個(gè)函數(shù)來(lái)確定消息是否涉及更高級(jí)別(至少有一個(gè)路由點(diǎn)在 350 級(jí)以上)。

糟糕的代碼是這樣實(shí)現(xiàn)的:

func IsUpperLevel(m Message) bool {
    for _, r := range m.RoutePoints {
        if r.FlightLevel > upperLevel {
            return true
        }
    }

    return false
}

意味著我們必須將消息作為函數(shù)的輸入?yún)?shù)傳遞。 而良好的代碼只是一個(gè)帶有消息接收器的函數(shù):

func (m *Message) IsUpperLevel() bool {
    for _, r := range m.RoutePoints {
        if r.FlightLevel > upperLevel {
            return true
        }
    }

    return false
}

第二種方法更可取。我們只需指示消息結(jié)構(gòu)實(shí)現(xiàn)了特定的行為。

這也可能是使用 Go 接口的第一步。例如,如果將來(lái)我們需要?jiǎng)?chuàng)建另一個(gè)具有相同行為(IsUpperLevel())的結(jié)構(gòu)體,初始代碼甚至不需要重構(gòu)(因?yàn)橄⒁呀?jīng)實(shí)現(xiàn)了這個(gè)行為)。

12.注釋

這是相當(dāng)明顯的,但糟糕的注釋寫得很糟糕。

另一方面,我嘗試像在實(shí)際項(xiàng)目中那樣注釋良好的代碼。盡管我不是喜歡每一行都注釋的開(kāi)發(fā)者,但我仍然認(rèn)為至少對(duì)每個(gè)函數(shù)和復(fù)雜函數(shù)中的主要步驟進(jìn)行注釋是重要的。

舉個(gè)例子:

// Split each line in a goroutine
for _, line := range in {
    go mapLine(line, ch)
}

msg := Message{}

// Gather the goroutine results
for range in {
    // ...
}

除了函數(shù)注釋之外,一個(gè)具體的例子也可能非常有用:

// 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) {
    // ...
}

這樣具體的例子可以幫助其他開(kāi)發(fā)人員更好地理解現(xiàn)有項(xiàng)目。

最后但同樣重要的是,根據(jù) Go 的最佳實(shí)踐,包本身也應(yīng)進(jìn)行注釋。

/*
Package good is a library for parsing the ADEXP messages.
An intermediate format Message is built by the parser.
*/

package good

13.日志記錄

另一個(gè)顯而易見(jiàn)的例子是糟糕代碼中缺乏生成的日志。因?yàn)槲也皇菢?biāo)準(zhǔn)日志包的粉絲,所以在這個(gè)項(xiàng)目中我使用了一個(gè)名為 logrus 的外部庫(kù)。

14.go fmt

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

15.DDD

領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)引入了普遍語(yǔ)言的概念,強(qiáng)調(diào)了在整個(gè)項(xiàng)目參與者(業(yè)務(wù)專家、開(kāi)發(fā)人員、測(cè)試人員等)之間使用共享語(yǔ)言的重要性。在這個(gè)例子中無(wú)法真正衡量這一點(diǎn),但保持像 Message 這樣的簡(jiǎn)單結(jié)構(gòu)符合領(lǐng)域邊界內(nèi)部使用的語(yǔ)言也是提高整體項(xiàng)目可維護(hù)性的一個(gè)好方法。

16.性能結(jié)果

在 i7–7700 4x 3.60Ghz 上,我進(jìn)行了基準(zhǔn)測(cè)試來(lái)比較兩個(gè)解析器:

  • 糟糕的實(shí)現(xiàn):60430 納秒/操作
  • 良好的實(shí)現(xiàn):45996 納秒/操作

糟糕的代碼比良好的代碼慢了超過(guò)30%。

結(jié)論

在我看來(lái),很難給出糟糕代碼和良好代碼的一般定義。在一個(gè)上下文中的代碼可能被認(rèn)為是好的,而在另一個(gè)上下文中可能被認(rèn)為是糟糕的。

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

同時(shí),對(duì)于開(kāi)發(fā)人員來(lái)說(shuō),關(guān)心簡(jiǎn)單、易維護(hù)和高效的代碼也很重要。

性能改進(jìn)并非憑空而來(lái),它伴隨著代碼復(fù)雜性的增加。

一個(gè)優(yōu)秀的開(kāi)發(fā)人員是能夠在特定的上下文中找到這些特性之間的平衡的人。

就像在 DDD 中一樣,上下文是關(guān)鍵的。

責(zé)任編輯:趙寧寧 來(lái)源: 技術(shù)的游戲
相關(guān)推薦

2012-12-28 09:47:07

程序員代碼編程

2015-06-30 08:31:59

舊代碼重寫

2010-09-08 09:23:49

#region指令C#

2013-09-24 10:20:35

代碼代碼異味

2017-10-09 12:05:57

優(yōu)秀的代碼代碼量糟糕的代碼

2014-05-16 10:51:33

科學(xué)代碼最佳實(shí)踐

2009-09-15 16:16:35

代碼習(xí)慣

2023-12-18 10:01:40

Golang代碼開(kāi)發(fā)

2023-12-19 22:40:23

Golang編程函數(shù)

2018-11-05 08:53:25

代碼開(kāi)發(fā)內(nèi)核

2018-02-25 11:00:34

代碼開(kāi)發(fā)程序員

2015-08-31 10:14:30

程序員處理代碼糟糕代碼

2015-09-01 11:20:58

程序員糟糕代碼

2013-12-04 14:19:40

JavaScript代碼重用

2025-06-04 08:15:00

Python編程代碼

2010-02-26 13:27:57

Python 代碼開(kāi)發(fā)

2021-04-22 15:08:01

代碼評(píng)審郵件

2021-10-10 23:02:49

Golang語(yǔ)言代碼

2017-05-31 14:14:11

互聯(lián)網(wǎng)

2023-05-29 09:25:38

GolangSelect
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 中文字幕成人av | 天天干视频在线 | 成人精品在线观看 | 中文字幕97 | 91色在线 | 成人精品鲁一区一区二区 | 久久久久中文字幕 | 久久69精品久久久久久久电影好 | www国产亚洲精品久久网站 | 美女国产一区 | 欧美在线视频一区二区 | 久久久久久久久久久一区二区 | 一级黄色网页 | 欧美中文字幕一区二区三区 | 国产伦精品一区二区三区精品视频 | 美女在线一区二区 | 精品乱码久久久久 | 国产在线一区二区三区 | 欧美片网站免费 | 91色在线视频 | 久久精品在线免费视频 | 麻豆久久久9性大片 | 91欧美精品| 亚洲欧美日本国产 | 日韩欧美大片在线观看 | 久久久久中文字幕 | 国产精品久久久免费 | 欧美黑人狂野猛交老妇 | 欧美淫片 | 精品一区av | 亚洲日韩中文字幕一区 | 日韩爱爱网站 | 亚洲高清在线 | 亚洲精品免费在线观看 | 久久久久国产精品一区二区 | 日韩视频在线观看中文字幕 | 青青草综合网 | 亚洲欧美在线观看视频 | 国产极品粉嫩美女呻吟在线看人 | 国产福利在线视频 | 久久久噜噜噜www成人网 |