本節課講了如何寫出更簡潔清晰的代碼,每種語言都有自己的特性,也有自己獨特的代碼規範,對於 Go 來說,有哪些性能優化的手段、趁手的工具,也都進行了介紹。
高質量代碼需要具備正確可靠、簡潔清晰的特性
- 正確性:各種邊界條件是否考慮完備、錯誤的調用能否被處理
- 可靠性:異常情況或錯誤處理明確,依賴的服務異常能夠及時處理
- 簡潔:邏輯是否簡單、後續新增功能是否能夠快速支持
- 清晰可讀:其他人閱讀理解代碼時是否能清楚明白、重構時是否不會擔心出現無法預料的情況
而這就需要編碼規範。
編碼規範#
格式化工具#
提到編碼規範就不得不提到代碼格式化工具,推薦使用 Go 官方提供的格式化工具 gofmt
,Goland 中內置了其功能,常見的 IDE 也都能方便的配置
- 另一個工具是
goimports
,相當於gofmt
加上依賴包的管理,自動增刪依賴的包。
js 中也有類似的格式化工具
Prettier
,可以配合 ESLint 進行代碼格式化。
注釋規範#
好的注釋需要
-
解釋代碼作用
-
解釋複雜、不明顯的邏輯
-
解釋代碼實現的原因(這些因素脫離上下文後很難理解)
-
解釋代碼什麼情況會出錯(解釋一些限制條件)
-
解釋公共符號的注釋(包中聲明的每個公共的符號:變量、常量、函數以及結構等)
- 例外:不需要注釋實現接口的方法
Google Style 指南中有兩條規則:
- 任何既不明顯也不簡短的 公共功能 必須予以注釋。
- 無論長度或複雜程度如何,對 庫 中的任何函數都必須進行注釋
而需要避免的情況如下:
- 對可見名知義的函數進行囉嗦的注釋
- 對顯而易見的流程進行直接翻譯
總而言之,代碼是最好的注釋
- 注釋應該提供 代碼未表達出的上下文信息
- 簡潔清晰的代碼對流程注釋沒有要求,但是對於為什麼這麼做,代碼的相關背景等可以通過注釋補充,提供有效信息。
命名規範#
變量名#
-
簡潔勝於冗長
i
和index
的作用範圍,不需要index
的額外冗長
// Bad
for index := 0; index < len(s) ; index++ {
// do something
}
// Good
for i := 0; i < len(s); i++ {
// do something
}
-
縮略詞全大寫,但當其 位於變量開頭且不需要導出 時,使用全小寫
- 如使用
ServeHTTP
而不是ServeHttp
- 使用
XMLHTTPRequest
或xmlHTTPRequest
- 如使用
-
變量名距離其被使用的地方越遠,則越需要攜帶越多的上下文信息。
- 如全局變量,在其名字中需要更多的上下文信息,使得在不同地方可以輕易辨認出其含義
// 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
- 如 http 包中創建服務的函數,
-
函數名 盡量簡短
-
當名為
foo
的包某個函數返回類型T
時 (T
並不是Foo
),可以在函數名中加入返回的類型信息- 返回
Foo
類型時,可以省略而不導致歧義
- 返回
包名#
- 只由小寫字母組成。不包含大寫字母和下劃線等字符
- 簡短並包含一定的上下文信息。例如
schema
、task
等 - 不要與標準庫同名。例如不要使用
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
}
-
複雜錯誤:使用錯誤的
Wrap
和Unwrap
- 錯誤的
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
-
當程序啟動階段發生不可逆轉的錯誤時,可以在
init
或main
函數中使用panic
(sarama/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.Builder
,bytes.Buffer
的內存是以倍數申請的 -
strings.Builder
和bytes.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...)
相關鏈接#
- 《golang pprof 實戰》代碼實驗用例: github.com/wolfogre/go…
- 嘗試使用 test 命令,編寫並運行簡單測試 go.dev/doc/tutoria…
- 嘗試使用 -bench 參數,對編寫的函數進行性能測試,pkg.go.dev/testing#hdr…
- Go 代碼 Review 建議 github.com/golang/go/w…
- Uber 的 Go 編碼規範,github.com/uber-go/gui…
總結及心得#
本節課介紹了 Go 乃至其他語言中常見的代碼規範,提出了 Go 語言中相關的性能優化建議。後續還進行了性能優化的實戰練習,使用 pprof 工具進行。
筆記內容來源於第三屆青訓營張雷老師的課程《高質量編程與性能調優實戰》
課程資料:【Go 語言原理與實踐學習資料(上)】第三屆字節跳動青訓營 - 後端專場