Reviewbot 開源 | 這些寫 Go 程式碼的小技巧,你都知道嗎?
Reviewbot 是七牛雲開源的一個專案,旨在提供一個自託管的程式碼審查服務, 方便做 code review/靜態檢查, 以及自定義工程規範的落地。
自從上了 Reviewbot 之後,我發現有些 lint 錯誤,還是很容易出現的。比如
dao/files_dao.go:119:2: `if state.Valid() || !start.IsZero() || !end.IsZero()` has complex nested blocks (complexity: 6) (nestif)
cognitive complexity 33 of func (*GitlabProvider).Report is high (> 30) (gocognit)
這兩個檢查,都是圈複雜度相關。
圈複雜度(Cyclomatic complexity)是由 Thomas McCabe 提出的一種度量程式碼複雜性的指標,用於計算程式中線性獨立路徑的數量。它透過統計程式控制流中的判定節點(如 if、for、while、switch、&&、|| 等)來計算。圈複雜度越高,表示程式碼路徑越多,測試和維護的難度也就越大。
圈複雜度高的程式碼,往往意味著程式碼的可讀性和可維護性差,非常容易出 bug。
為什麼這麼說呢?其實就跟人腦處理資訊一樣,一件事情彎彎曲曲十八繞,當然容易讓人暈。
所以從工程實踐角度,我們希望程式碼的圈複雜度不能太高,畢竟絕大部分程式碼不是一次性的,是需要人來維護的。
那該怎麼做呢?
這裡我首先推薦一個簡單有效的方法:Early return。
Early return - 邏輯展平,減少巢狀
Early return, 也就是提前返回,是我個人認為最簡單,日常很多新手同學容易忽視的方法。
舉個例子:
func validate(data *Data) error {
if data != nil {
if data.Field != "" {
if checkField(data.Field) {
return nil
}
}
}
return errors.New("invalid data")
}
這段程式碼的邏輯應該挺簡單的,但巢狀層級有點多,如果以後再複雜一點,就容易出錯。
這種情況就可以使用 early return 模式改寫,把這個巢狀展平:
func validate(data *Data) error {
if data == nil {
return errors.New("data is nil")
}
if data.Field == "" {
return errors.New("field is empty")
}
if !checkField(data.Field) {
return errors.New("field validation failed")
}
return nil
}
是不是清晰很多,看著舒服多了?
記住這裡的訣竅:如果你覺得順向思維寫出的程式碼有點繞,且巢狀過多的話,就可以考慮使用 early return 來反向展平。
當然,嚴格意義上講,early return 只能算是一種小技巧。要想寫出高質量的程式碼,最重要的還是理解 分層、組合、單一職責、高內聚低耦合、SOLID 原則等 這些核心設計理念 和 設計模式了。
Functional Options 模式 - 引數解耦
來看一個場景: 方法引數很多,怎麼辦?
比如這種:
func (s *Service) DoSomething(ctx context.Context, a, b, c, d int) error {
// ...
}
有一堆引數,而且還是同型別的。如果在呼叫時,一不小心寫錯了引數位置,就很麻煩,因為編譯器並不能檢查出來。
當然,即使不是同型別的,引數多了可能看著也不舒服。
怎麼解決?
這種情況,可以選擇將引數封裝成一個結構體,這樣在使用時就會方便很多。封裝成結構體後還有一個好處,就是以後增刪引數時(結構體的屬性),方法簽名不需要修改。避免了以前需要改方法簽名時,呼叫方也需要跟著到處改的麻煩。
不過,在 Go 語言中,還有一種更優雅的解決方案,那就是Functional Options 模式。
不管是 Rob Pike 還是 Dave Cheney 以及 uber 的 go guides 中都有專門的推薦。
- https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html
- https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
- https://github.com/uber-go/guide/blob/master/style.md#functional-options
這種模式,本質上就是利用了閉包的特性,將引數封裝成一個匿名函式,有諸多妙用。
Reviewbot 自身的程式碼中,就有相關的使用場景 (https://github.com/qiniu/reviewbot/blob/c354fde07c5d8e4a51ddc8d763a2fac53c3e13f6/internal/lint/providergithub.go#L263),比如:
// GithubProviderOption allows customizing the provider creation.
type GithubProviderOption func(*GithubProvider)
func NewGithubProvider(ctx context.Context, githubClient *github.Client, pullRequestEvent github.PullRequestEvent, options ...GithubProviderOption) (*GithubProvider, error) {
// ...
for _, option := range options {
option(p)
}
// ...
if p.PullRequestChangedFiles == nil {
// call github api to get changed files
}
// ...
}
這裡的 options
就是 functional options 模式,可以靈活地傳入不同的引數。
當時之所以選擇這種寫法,一個重要的原因是方便單測書寫。
為什麼這麼說呢?
看上述程式碼能知道,它需要呼叫 github api 去獲取 changed files
, 這種實際依賴外部的場景,在單測時就很麻煩。但是,我們用了 functional options 模式之後,就可以透過 p.PullRequestChangedFiles
是否為 nil 這個條件,靈活的繞過這個問題。
Functional Options 模式的優點還有很多,總結來講 (from dave.cheney):
- Functional options let you write APIs that can grow over time.
- They enable the default use case to be the simplest.
- They provide meaningful configuration parameters.
- Finally they give you access to the entire power of the language to initialize complex values.
現在大模型相關的程式碼,能看到很多 functional options 的影子。比如
https://github.com/tmc/langchaingo/blob/238d1c713de3ca983e8f6066af6b9080c9b0e088/llms/ollama/options.go#L25
type Option func(*options)
// WithModel Set the model to use.
func WithModel(model string) Option {
return func(opts *options) {
opts.model = model
}
}
// WithFormat Sets the Ollama output format (currently Ollama only supports "json").
func WithFormat(format string) Option {
// ...
}
// If not set, the model will stay loaded for 5 minutes by default
func WithKeepAlive(keepAlive string) Option {
// ...
}
所以建議大家在日常寫程式碼時,也多有意識的嘗試下。
善用 Builder 模式/策略模式/工廠模式,消弭複雜 if-else
Reviewbot 目前已支援兩種 provider(github 和 gitlab),以後可能還會支援更多。
而因為不同的 Provider 其鑑權方式還可能不一樣,比如:
- github 目前支援 Github APP 和 Personal Access Token 兩種方式
- gitlab 目前僅支援 Personal Access Token 方式
當然,還有 OAuth2 方式,後面 reviewbot 也也會考慮支援。
那這裡就有一個問題,比如在 clone 程式碼時,該使用哪種方式?程式碼該怎麼寫?使用 token 的話,還有個 token 過期/重新整理的問題,等等。
如果使用 if-else 模式來實現,程式碼就會變得很複雜,可讀性較差。類似這種:
if provider == "github" {
// 使用 Github APP 方式
if githubClient.AppID != "" && githubClient.AppPrivateKey != "" {
// 使用 Github APP 方式
// 可能需要呼叫 github api 獲取 token
} else if githubClient.PersonalAccessToken != "" {
// 使用 Personal Access Token 方式
// 可能需要呼叫 github api 獲取 token
} else {
return err
}
} else if provider == "gitlab" {
// 使用 Personal Access Token 方式
if gitlabClient.PersonalAccessToken != "" {
// 使用 Personal Access Token 方式
// 可能需要呼叫 gitlab api 獲取 token
} else {
return errors.New("gitlab personal access token is required")
}
}
但現在 Reviewbot 的程式碼中,相關程式碼僅兩行:
func (s *Server) handleSingleRef(ctx context.Context, ref config.Refs, org, repo string, platform config.Platform, installationID int64, num int, provider lint.Provider) error {
// ...
gb := s.newGitConfigBuilder(ref.Org, ref.Repo, platform, installationID, provider)
if err := gb.configureGitAuth(&opt); err != nil {
return fmt.Errorf("failed to configure git auth: %w", err)
}
// ...
}
怎麼做到的呢?
其實是使用了 builder 模式,將 git 的配置和建立過程封裝成一個 builder,然後根據不同的 provider 選擇不同的 builder,從而消弭了複雜的 if-else 邏輯。
當然內部細節還很多,不過核心思想都是將複雜的邏輯封裝起來,在主互動邏輯中,只暴露簡單的使用介面,這樣程式碼的可讀性和可維護性就會大大提高。
最後
到底如何寫出高質量的程式碼呢?這可能是很多有追求的工程師,一直在思考的問題。
在我看來,可能是沒有標準答案的。不過呢,知道一些技巧,並能在實戰中靈活運用,總歸是好的。
你說是吧?
相關文章
- 收藏!這些 IDE 使用技巧,你都知道嗎IDE
- iOS這些小技巧你都知道嗎iOS
- 這些操作刪除console.log程式碼,你都知道嗎
- 這幾個好用的Python開源框架,你都知道嗎?Python框架
- Chrome DevTools中的這些騷操作,你都知道嗎?Chromedev
- 這些Python程式碼技巧,你肯定還不知道Python
- 關於Linux系統,這些你都知道嗎?Linux
- 天天寫 SQL,這些神奇的特性你知道嗎?SQL
- 學習Python這些面試題你都知道嗎?Python面試題
- 超好用的macOS Monterey 隱藏功能,這些你都知道嗎Mac
- Go 泛型的這 3 個核心設計,你都知道嗎?Go泛型
- 這些.NET開源專案你知道嗎?讓.NET開源來得更加猛烈些吧!
- redis為什麼變慢了?這些原因你都知道嗎Redis
- 這些開源CMS,你敢用嗎?
- 這些技巧你知道嗎?macOS的Fn鍵實用秘訣Mac
- 你說你懂計算機網路,那這些你都知道嗎計算機網路
- PbootCms導航選單標籤的這些小技巧你都知道嗎?boot
- 小程式的這些坑你踩過嗎?
- 消除遊戲美術設計的這些套路,你都知道嗎?遊戲
- 你需要知道的小程式開發技巧
- 身為初學Java的你,這些IDE的優缺點你都知道嗎?JavaIDE
- 關於等級保護測評,這些你都知道嗎?
- 網際網路都在講的敏捷開發,這些敏捷開發流程你都知道嗎?敏捷
- SSD固態硬碟使用的五個誤區,這些你都知道嗎?硬碟
- 這些Python騷操作,你知道嗎?Python
- 你知道嗎?元宇宙收益的主要來源是這些行業……元宇宙行業
- 這些手寫程式碼會了嗎?少年
- 遊戲影視美術設計也有套路,這些你都知道嗎?遊戲
- 第四篇:Hyperion安裝配置,這些細節你都知道嗎
- 你知道黑客的入侵方式都有哪些嗎?這些你知道幾個?黑客
- 關於Python學習的方法以及技巧,你都知道嗎?Python
- 你會犯這些 Go 編碼錯誤嗎(二)?Go
- 單例模式的七種寫法,你都知道嗎?單例模式
- 軟體測試這些你知道嗎?
- 這些免費OA陷阱你知道嗎?
- 這些CSS提效技巧,你需要知道!CSS
- 小特性 大用途 —— YashanDB JDBC驅動的這些特性你都get了嗎?JDBC
- ES6的這些操作技巧,你會嗎?