banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Go語言初上手(三)編碼規範與性能優化 | 青訓營

本節課講了如何寫出更簡潔清晰的代碼,每種語言都有自己的特性,也有自己獨特的代碼規範,對於 Go 來說,有哪些性能優化的手段、趁手的工具,也都進行了介紹。

高質量代碼需要具備正確可靠、簡潔清晰的特性

  • 正確性:各種邊界條件是否考慮完備、錯誤的調用能否被處理
  • 可靠性:異常情況或錯誤處理明確,依賴的服務異常能夠及時處理
  • 簡潔:邏輯是否簡單、後續新增功能是否能夠快速支持
  • 清晰可讀:其他人閱讀理解代碼時是否能清楚明白、重構時是否不會擔心出現無法預料的情況
    而這就需要編碼規範。

編碼規範#

格式化工具#

提到編碼規範就不得不提到代碼格式化工具,推薦使用 Go 官方提供的格式化工具 gofmt,Goland 中內置了其功能,常見的 IDE 也都能方便的配置

  • 另一個工具是 goimports,相當於 gofmt 加上依賴包的管理,自動增刪依賴的包。

image.png

js 中也有類似的格式化工具 Prettier,可以配合 ESLint 進行代碼格式化。

注釋規範#

好的注釋需要

  • 解釋代碼作用

  • 解釋複雜、不明顯的邏輯

  • 解釋代碼實現的原因(這些因素脫離上下文後很難理解)

  • 解釋代碼什麼情況會出錯(解釋一些限制條件)

  • 解釋公共符號的注釋(包中聲明的每個公共的符號:變量、常量、函數以及結構等)

    • 例外:不需要注釋實現接口的方法

Google Style 指南中有兩條規則:

  • 任何既不明顯也不簡短的 公共功能 必須予以注釋。
  • 無論長度或複雜程度如何,對 中的任何函數都必須進行注釋

而需要避免的情況如下:

  • 對可見名知義的函數進行囉嗦的注釋
  • 對顯而易見的流程進行直接翻譯

總而言之,代碼是最好的注釋

  • 注釋應該提供 代碼未表達出的上下文信息
  • 簡潔清晰的代碼對流程注釋沒有要求,但是對於為什麼這麼做,代碼的相關背景等可以通過注釋補充,提供有效信息。

命名規範#

變量名#

  • 簡潔勝於冗長

    • iindex 的作用範圍,不需要 index 的額外冗長
// Bad
for index := 0; index < len(s) ; index++ {
    // do something
}
// Good
for i := 0; i < len(s); i++ {
    // do something
}
  • 縮略詞全大寫,但當其 位於變量開頭且不需要導出 時,使用全小寫

    • 如使用 ServeHTTP 而不是 ServeHttp
    • 使用 XMLHTTPRequestxmlHTTPRequest
  • 變量名距離其被使用的地方越遠,則越需要攜帶越多的上下文信息。

    • 如全局變量,在其名字中需要更多的上下文信息,使得在不同地方可以輕易辨認出其含義
// Bad
func ( c *Client ) send( req *Request, t time.Time )

// Good
func ( c *Client ) send( req *Request, deadline time.Time )

函數命名#

  • 函數名 不攜帶包名的上下文信息,因為包名和函數名總是成對出現的

    • 如 http 包中創建服務的函數, Serve > ServeHTTP,因為調用時總是http.Serve
  • 函數名 盡量簡短

  • 當名為 foo 的包某個函數返回類型 T 時 (T並不是 Foo ),可以在函數名中加入返回的類型信息

    • 返回Foo類型時,可以省略而不導致歧義

包名#

  • 只由小寫字母組成。不包含大寫字母和下劃線等字符
  • 簡短並包含一定的上下文信息。例如schematask
  • 不要與標準庫同名。例如不要使用 sync 或者 strings 以下規則盡量滿足,以標準庫包名為例:
  • 不使用常用變量名作為包名。例如使用 bufio 而不是 buf
  • 使用單數而不是複數。例如使用 encoding 而不是 `encodings``
  • 謹慎地使用縮寫。例如使用 fmt 在不破壞上下文的情況下比 format 更加簡短

總的來說,好的命名降低閱讀理解代碼的成本,可以讓人把關注點留在主流程上,清晰地理解程序的功能,而不是頻繁切換到分支細節,並且必須解釋它。

控制流程#

  • 避免嵌套,保持正常流程清晰可讀

    • 優先處理錯誤情況 / 特殊情況,儘早返回或繼續循環來減少嵌套
 // Bad
 if foo {
    return x
 } else {
    return nil
 }
 ​
 // Good
 if foo {
    return x
 }
 return nil
  • 儘量保持正常代碼路徑為最小縮進,減少嵌套
 // Bad
 func OneFunc() error {
    err := doSomething()
    if err == nil {
       err := doAnotherThing()
       if err == nil {
          return nil // normal case
       }
       return err
    }
    return err
 }
 ​
 // Good
 func OneFunc() error {
    if err := doSomething(); err != nil {
       return err
    }
    if err := doSomething(); err != nil {
       return err
    }
    return nil // normal case
 }

總而言之,程序中流程這一塊處理邏輯儘量走直線,避免複雜的嵌套分支,使正常流程代碼沿著屏幕向下移動。提升代碼可維護性和可讀性,因為故障問題大多出現在複雜的條件語句和循環語句中

錯誤處理#

  • 簡單錯誤

    • 簡單的錯誤指 僅出現一次 的錯誤,且在其他地方 不需要捕獲 該錯誤
    • 優先使用 errors.New 來創建匿名變量來直接表示簡單錯誤
    • 如果有格式化的需求,使用 fmt.Errorf
 func defaultCheckRedirect(req *Request, via []*Request) error {
    if len(via) >= 10 {
       return errors.New("stopped after 10 redirects")
    }
    return nil
 }
  • 複雜錯誤:使用錯誤的 WrapUnwrap

    • 錯誤的 Wrap 實際上是提供了一個 error 嵌套另一個 error 的能力,從而生成一個 error 的跟蹤鏈
    • fmt.Errorf 中使用 %w 關鍵字來將一個錯誤關聯至錯誤鏈中
    • 使用 errors.Is 判定錯誤是否為某特定錯誤,可判定錯誤鏈上的所有錯誤(go/wrap_test.go · golang/go
    • 使用 errors.As 在錯誤鏈上獲取特定種類的錯誤,並將錯誤賦值給定義好的變量。(go/wrap_test.go · golang/go

在 Go 中,比錯誤更嚴重的就是 panic,它的出現表示程序無法正常工作

  • 不建議在業務代碼中使用 panic

    • panic 發生後,會向上傳播至調用棧頂
    • 調用函數全都不包含 recover 會造成整個程序崩潰
    • 若問題可以被屏蔽或解決,建議使用 error 代替 panic
  • 當程序啟動階段發生不可逆轉的錯誤時,可以在 initmain 函數中使用 panicsarama/main.go · Shopify/sarama

painc,自然就會提到 recover,如果是引入其它庫的bug導致panic,影響到自身的邏輯時,就需要 recover

  • recover 只能在被 defer的函數中使用,嵌套無法生效,只在當前 goroutine 生效(github.com/golang/go/b…
  • defer 的語句是後進先出的。
  • 如果需要更多的上下文信息,可以 recover 後在 log 中記錄當前的調用棧(github.com/golang/webs…

小結#

  • error 要儘可能提供簡明的上下文信息鏈,方便定位問題
  • panic 用於真正異常的情況
  • recover 生效範圍,在當前 goroutine 的被 defer 的函數中生效

性能優化建議#

  • 前提:滿足正確可靠、簡潔清晰等質量因素的前提下,儘可能提高程序的效率
  • 折衷:有時候時間效率和空間效率可能對立,需要分析重要程度進行適當折衷。

針對 Go 語言特性,課上介紹了很多 Go 相關的性能優化建議:

預分配內存#

使用 make () 初始化切片時儘可能提供容量信息

 func PreAlloc(size int) {
    data := make([]int, 0, size)
    for k := 0; k < size; k++ {
       data = append(data, k)
    }
 }

這是由於切片本質是一個數組片段的描述,包括數組指針、片段的長度、片段的容量 (不改變內存分配情況下的最大長度),

  • 切片操作並不複製切片指向的元素
  • 創建一個新的切片會復用原來切片的底層數組 所以預先設置容量的值能夠避免額外的內存分配,獲得更好的性能

字符串處理優化#

使用 strings.Builder 常見的字符串拼接方式

  • + 進行連接 (最慢)

  • strings.Builder (最快)

  • bytes.Buffer 原理:字符串在 Go 語言中是不可變類型,佔用內存大小是固定的

  • 使用 + 拼接時,生成一個新的字符串,開辟一段新空間,新空間的大小是原來兩個字符串的大小之和

  • strings.Builderbytes.Buffer 的內存是以倍數申請的

  • strings.Builderbytes.Buffer 底層都是 []byte 陣列

    • bytes.Buffer 轉化為字符串時重新申請了一塊空間存放生成的字符串變量
    • strings.Builder 直接將底層的 []byte 轉換成了字符串類型返回
 func PreStrBuilder(n int, str string) string {
    var builder strings.Builder
    builder.Grow(n * len(str))
    for i := 0; i < n; i++ {
       builder.WriteString(str)
    }
    return builder.String()
 }

空結構體#

  • 空結構體 struct 實例不佔據任何的內存空間

  • 可作為各種場景下的佔位符使用

    • 節省內存空間
    • 空結構體本身具備很強的語義,即這裡不需要任何值,僅作為佔位符
  • 如實現 Set 時,利用 map 的鍵,而將值設為空結構體。(golang-set/threadunsafe...)

相關鏈接#

總結及心得#

本節課介紹了 Go 乃至其他語言中常見的代碼規範,提出了 Go 語言中相關的性能優化建議。後續還進行了性能優化的實戰練習,使用 pprof 工具進行。

筆記內容來源於第三屆青訓營張雷老師的課程《高質量編程與性能調優實戰》
課程資料:【Go 語言原理與實踐學習資料(上)】第三屆字節跳動青訓營 - 後端專場

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。