国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
真實案例(萬字長文):Bad Code vs Good Code in Golang

  預(yù)計閱讀時間: 23分鐘 建議收藏后閱讀

近來,經(jīng)常有人問我在Go 語言的項目里,什么樣的代碼算好代碼,什么樣的代碼算壞代碼。

我發(fā)現(xiàn)這個練習(xí)很有趣。嗯,這個項目太有趣了,所以我決定寫一篇博客和大家分享一下。

為了更好的和大家說明我的觀點,我利用一個我工作中碰到的問題為例,給大家講講。這個項目是一個空中流量控制系統(tǒng)(ATM)。

代碼我放在Github[1]上了。


背景

首先,我簡單介紹一下這個項目。

Eurocontrol 是歐洲國家管理空中飛機流量的一個機構(gòu)。通常在EurocontrolAir Navigation Service Provider (ANSP)直接交換數(shù)據(jù)的網(wǎng)絡(luò)是AFTN。這個網(wǎng)絡(luò)主要用來交換兩種不同的消息類型:ADEXPICAO。

其實我也不知道這兩類信息有啥區(qū)別,不過我們當(dāng)成兩類不同的信息就好了。

每種消息類型有它自己的格式,不過兩者傳達的信息都是等價的(或多或少)。

我們這個項目最關(guān)鍵的是性能。

這個項目會提供兩種解析ADEXP的Go 語言實現(xiàn):

  • 一種當(dāng)然是比較差的實現(xiàn)(包名是bad)
  • 一種是老司機的實現(xiàn)(當(dāng)然是好的啦,包名自然是good)

我們在這個練習(xí)里,只處理其中的一小部分。

我們的主要目的是為了說明問題,不是證明我有多強,對吧?


解析

簡單來說,ADEXP 消息里包含了一些指令,或者說關(guān)鍵字。比如:

-ARCID ACA878

ARCID的意思是飛行器(AiRcraft IDentifier)的識別碼是ACA878

-EETFIR EHAA 0853
-EETFIR EBBU 0908

這行的意思是FIRFlight Information Region)。

第一行FIREHAA 0853, 第二行的是EBBU 0908.

-GEO -GEOID GEO01 -LATTD 490000N -LONGTD 0500000W
-GEO -GEOID GEO02 -LATTD 500000N -LONGTD 0400000W

這里面的指令代碼是GEO,GEOID, LATTD, LONGTD。

為了提高處理的效率,我們自然希望通過并發(fā)的方式來處理這些連續(xù)的數(shù)據(jù)。

所以,基本我們的算法如下:

  1. 清洗數(shù)據(jù)(去除空格,去除注釋等等)
  2. 通過goroutine去分割每一行。每個goroutine處理一行,然后返回結(jié)果。
  3. 最后,把處理后的信息合并起來,生成新的信息。比如ADEXPICAO.

每個包包含一個名為adexp.go的文件。這個文件會把主要的函數(shù)ParseAdexpMessage()暴露出來。


咱們下面一步步來比較一下

讓我們現(xiàn)在一步步的來實現(xiàn)這個算法。通過兩種方式,我們可以比較一下那種寫法更好,那種寫法比較差,以及為什么。

String vs []byte

比較差的寫法是通過字符串輸入來處理。因為Go提供了強大的字節(jié)Bytes操作(如trimregexp等),我們可以把AFTN消息當(dāng)成通過TCP獲取到的消息,所以我們沒有理由把它轉(zhuǎn)換成字符串輸入。

譯者注:直接用byte處理會速度快一些

Error 管理

在差的實現(xiàn)的這個包里的代碼實現(xiàn)是非常糟糕的。(說的非常好,不然呢)。在第二個參數(shù)里返回的一些潛在的錯誤甚至沒有得到處理。

preprocessed, _ := preprocess(string)

好的代碼會處理各種潛在的錯誤:

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

如下的這種實現(xiàn)也不好,還有錯誤:

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

第一個錯誤是語法錯誤。根據(jù)Go語言規(guī)范,錯誤信息首字母不應(yīng)該大寫,結(jié)尾也不需要標(biāo)點符號。因為這個錯誤信息只是簡單的字符串,不需要格式化,所以用errors.New()性能更好。

敲黑板啦,所以好的實現(xiàn)應(yīng)該是這樣的:

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

避免嵌套

譯者注:嵌套是初級程序員跨不去的一個坎 :)

mapLine() 函數(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
    }
}

相反,好的實現(xiàn)可以盡量地避免嵌套:

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
}

用我的觀點來看,這樣的代碼更易讀一些。另外,錯誤處理也最好這樣實現(xiàn)。

例如:

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

再說一遍,這樣看起來好多了。

通過傳值或引用傳遞數(shù)據(jù)

典型的差的實現(xiàn)如下:

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

我們這個項目的背景是性能很重要。另外考慮到處理的數(shù)據(jù)可能很大,更好的選擇是傳遞container 結(jié)構(gòu)的指針。然而,在上面的例子中,是直接傳遞數(shù)據(jù)的。

因為采用了分片(一個簡單的忽略了底層數(shù)據(jù)的24字節(jié)的結(jié)構(gòu))處理,所以好的實現(xiàn)不存在這些問題。

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

通過傳值或傳引用的方式傳遞參數(shù)其實是一個和語言無關(guān)的事。通過傳值這種方式傳遞參數(shù)可以避免一些副作用,比如數(shù)據(jù)被修改,等等。

譯者注:如果大家對這點有點疑惑,需要去做做功課咯。傳值一般是復(fù)制一份數(shù)據(jù),傳遞到另外的函數(shù)去處理。這樣的缺點是浪費內(nèi)存,優(yōu)點是數(shù)據(jù)不會修改。傳遞指針(也就是引用的方式)優(yōu)點是節(jié)約內(nèi)存,缺點是數(shù)據(jù)可能被修改。我看很多C++程序員就是故意這樣去處理的。。。很費解。有C++程序員站出來解釋一下~

傳值還有一些優(yōu)點,比如在單元測試的時候,或者重構(gòu)并發(fā)的代碼等等(否則我們需要每次都檢查一下數(shù)據(jù)是否被修改)。

我非常相信具體在選擇傳值還是引用的問題上,一定要根據(jù)項目的背景去權(quán)衡。(誰說不是呢,外國人寫點東西廢話真不少。。。)

并發(fā)

這個差的實現(xiàn)其實是基于一個好的想法:通過goroutine去并行化處理數(shù)據(jù)(每個goroutine處理一行)。

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

這里采用了一個msg 這個共享內(nèi)存變量。

mapLine() 函數(shù)有三個參數(shù):

  • 最終返回的消息體msg的指針。mapLine()處理后的數(shù)據(jù)合并到msg變量。
  • 當(dāng)前處理的行
  • 用來發(fā)送處理結(jié)束通知的channel

給一個共享的Message變量發(fā)送指針違反了Go的原則:

不要通過共享內(nèi)存來通信,通過通信來共享內(nèi)存。

共享變量有兩個缺點:

  • 缺點#1:造成Slice的并發(fā)修改

因為可以并發(fā)地修改 Slice(兩個或兩個以上的go routine并發(fā)地修改),在差的實現(xiàn)中,我們不得不處理mutex。

例如,Message 包含 Estdata [] estdata。通過append 別的estdata修改slice,一定要這樣才行:

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

事實上,除了一些很特殊的場景,使用在go routine 中使用 mutex 可能本身就是臭代碼。

  • 缺點#2:假共享

因為潛在的假共享(一個CPU的core cache的cache line對于別的CPU core cache可能是無效的)的問題,所以通過線程或者goroutine 共享內(nèi)存不是個好主意。也就是說我們要盡可能地避免在不同的線程或goroutine間共享會被修改的變量。

雖然在這里例子中,因為輸入文件很小,所以假共享的作用看不出來。但是,以我來看,最好在開發(fā)者牢記這點。

讓我們看看好的實現(xiàn)怎么處理并行:

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

現(xiàn)在mapLine() 有兩個輸入:

  • 當(dāng)前行
  • channel。這次channel不僅會發(fā)送一下執(zhí)行的結(jié)果,而且會返回執(zhí)行的結(jié)果。這也意味著goroutine不會修改Message的結(jié)構(gòu)。

處理結(jié)果的goroutine的是父goroutine。


msg := Message{}

for range in {
    data := <-ch

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

我認(rèn)為這樣實現(xiàn)最好,更符合Go語言僅僅通過通信去共享內(nèi)存的原則。

一個可能被吐槽的點是每行都spawn一個goroutine。之所以這樣實現(xiàn)是因為ADEXP 消息只包含幾千行,所以也不至于導(dǎo)致goroutine爆發(fā)。更好的選擇是創(chuàng)建一個可復(fù)用的goroutine池。

行處理結(jié)束后通知

在差的實現(xiàn)中,就像上面描述的一樣,一旦mapLine()處理結(jié)束后,我們需要通知父goroutine。這是通過chan string實現(xiàn)的:

ch <- 'ok'

因為父routine沒有去檢查channel返回的結(jié)果,更好的選擇可能是使用chan struct{}, ch <- struct{}{}或者其他更好的選擇(GC wise),比如chan interface{},返回ch <- nil。

If

Go 允許在判斷條件前加其他語句。

如下的代碼,

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

可以簡寫一下:

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

經(jīng)過重構(gòu),提高了一點可讀性,有沒有呢?

Switch

差的實現(xiàn)中,switch沒有添加處理默認(rèn)情況。

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

雖然說對于開發(fā)者來說,默認(rèn)值是可選的。不過假如有默認(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)值,有助于盡可能地避免開發(fā)者開發(fā)過程中產(chǎn)生的錯誤。

遞歸

parseComplexLines()函數(shù)是用來解析復(fù)雜的token。差的實現(xiàn)中使用了遞歸。

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不支持tail-call(尾調(diào)用,在函數(shù)體末尾返回另一個函數(shù)的調(diào)用)優(yōu)化子函數(shù)調(diào)用。好的實現(xià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}
}

第二種實現(xiàn)比第一種性能更好。

常量管理

我們需要把ADEXP和ICAO消息分開。差的實現(xiàn)如下:

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

更優(yōu)雅的Go實現(xiàn)如下:

const (
    AdexpType = iota
    IcaoType 
)

它們的計算結(jié)果是一樣的,下面的這種寫法可以減少潛在的程序員犯的錯。

Receiver function

每個解析器提供了函數(shù)來判斷消息是否是超過了upper level(至少一個點超過了level 350)。

比較差的代碼實現(xiàn)如下:

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

    return false
}

這意味著我們必須把Message傳遞給函數(shù)。然而好的代碼使用Message receiver 簡化了函數(shù):


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

    return false
}

下面的這些實現(xiàn)更受人喜歡。我們給Message struct 添加了具體的實現(xiàn)。

它可能也是使用Go interface的第一步。假如有一天我們需要創(chuàng)建具有同樣行為(IsUpperLevel())的另一個struct,甚至不需要重構(gòu)初始化的代碼(因為Message已經(jīng)implement這個實現(xiàn)了)。

注釋

還有一個很明顯的問題就是差的代碼注釋很少。

另一方面,我盡量像我在真實項目中一樣注釋“好的代碼”。即使這樣,我也不是那種會注釋每一行代碼的開發(fā)者,我仍然認(rèn)為在一個復(fù)雜的函數(shù)里,至少每個函數(shù)以及主要的步驟都應(yīng)該注釋一下。

例如:


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

msg := Message{}

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

關(guān)于注釋一個很有說服力的例子如下:


// 解析一行并返回header(token名)和值
// 例如: -COMMENT TEST 應(yīng)該返回 COMMENT 和 TEST (in byte slices)
func parseLine(in []byte) ([]byte, []byte) {
    // ...
}

這樣的代碼真的可以幫助到其他程序員更好的理解現(xiàn)存的項目

雖然最後才說,但不表示它最不重要,根據(jù)Go 的最佳實踐,package最好也需要注釋一下。

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


package good

日志

另一個顯而易見的問題是在差的實現(xiàn)里缺少日志。因為我不喜歡Go標(biāo)準(zhǔn)庫的log package,所以我使用了Logrus[2].

go fmt

Go 提供了很好的格式化工具,go fmt。很不幸,我們忘記了在差的實現(xiàn)上執(zhí)行了(譯者注:你確定不是故意的嗎?),但是我們再好的代碼上執(zhí)行了。(譯者注:好意外?。?/p>

DDD

(略)

性能比較結(jié)果

在一臺 i7–7700 4x 3.60Ghz機子上,我分別跑了一下兩個解析器:

  • 差的實現(xiàn):60430 ns/op
  • 好的實現(xiàn):45996 ns/op

差的代碼比好的實現(xiàn)慢30%。


結(jié)論

對我來說,其實定義好的實現(xiàn)和差的實現(xiàn)是比較難的。離開場景談應(yīng)用都是耍流氓。

好的代碼的第一個特征是根據(jù)需求提供正確的解決方案。一個不滿足需求的代碼,哪怕性能再好,也沒什么卵用。簡單、可維護、性能好的代碼是值得開發(fā)者時刻關(guān)注的幾個點。

性能不會憑空提升,它和代碼的復(fù)雜度增加相關(guān)。

好的開發(fā)者能夠根據(jù)需求和這些特點中找到平衡。

和DDD一樣,context是關(guān)鍵?。海?/p>

同時,對于開發(fā)者來說

附錄:

ADEXP 文件如下:

-TITLE IFPL
-ADEP CYYZ
-ALTNZ EASTERN :CREEK'()+,./
-ADES AFIL
-ARCID ACA878
-ARCTYP A333
-CEQPT SDE3FGHIJ3J5LM1ORVWXY
-EETFIR KZLC 0035
-EETFIR KZDV 0131
-EETFIR KZMP 0200
-EETFIR CZWG 0247
-EETFIR CZUL 0349
-EETFIR CZQX 0459
-EETFIR EGGX 0655
-EETFIR EGPX 0800
-EETFIR EGTT 0831
-EETFIR EHAA 0853
-EETFIR EBBU 0908
-EETFIR EDGG 0921
-EETFIR EDUU 0921
-ESTDATA -PTID XETBO -ETO 170302032300 -FL F390
-ESTDATA -PTID ARKIL -ETO 170302032300 -FL F390
-GEO -GEOID GEO01 -LATTD 490000N -LONGTD 0500000W
-GEO -GEOID GEO02 -LATTD 500000N -LONGTD 0400000W
-GEO -GEOID GEO04 -LATTD 520000N -LONGTD 0200000W
-BEGIN RTEPTS
       -PT -PTID CYYZ -FL F000 -ETO 170301220429
       -PT -PTID JOOPY -FL F390 -ETO 170302002327
       -PT -PTID GEO01 -FL F390 -ETO 170302003347
       -PT -PTID BLM -FL F171 -ETO 170302051642
       -PT -PTID LSZH -FL F014 -ETO 170302052710
-END RTEPTS
-SPEED N0456 ARKIL
-SPEED N0457 LIZAD
-MSGTXT (ACH-BEL20B-LIML1050-EBBR-DOF/150521-14/HOC/1120F320 -18/PBN/B1 DOF/150521 REG/OODWK RVR/150 OPR/BEL ORGN/LSAZZQZG SRC/AFP RMK/AGCS EQUIPPED)
-COMMENT ???FPD.F15: N0410F300 ARLES UL153 PUNSA/N0410F300 UL153
VADEM/N0400F320 UN853 PENDU/N0400F330 UN853 IXILU/N0400F340 UN853
DIK/N0400F320 UY37 BATTY

原文鏈接:https://teivah.medium.com/good-code-vs-bad-code-in-golang-84cb3c5da49d 

作者:Teiva Harsanyi

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
Go 語言簡介(下)— 特性
手把手教姐姐寫消息隊列
【Go實戰(zhàn) | 電商平臺】(5) 用戶登錄
用Golang實現(xiàn) echo服務(wù)器/客戶端
NSQ源碼剖析之nsqd
在Go中,你犯過這些錯誤嗎
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服