Go 語言最佳實踐建議
【導讀】go 語言實戰踩坑經驗!本文詳細介紹了實際項目開發中的實用規則。在開發中遵守規範也是保持良好團隊合作的必要操作,一起來學習吧!
一. 介紹
每種語言都會有基本的語言規範,本文將會介紹 Go 語言實戰建議 Practical Go: Real world advice for writing maintainable Go programs (https://dave.cheney.net/practical-go/presentations/qcon-china.html#_introduction)
二. 指導原則
Go 語言有以下 3 點基本指導原則
-
簡單性: 簡單性是 Go 語言的最高目標,無論我們編寫什麼程序,我們都應該同意這一點
它們很簡單
。很多情況下我們都害怕遇到一個問題就是我不懂這段代碼,不知如何修復,這會導致軟件複雜不可靠。
-
可讀性: 可讀性很重要,因爲所有軟件不僅僅是 Go 語言程序都是由人類編寫的,供他人閱讀。執行軟件的計算機則是次要的。代碼的讀取次數比寫入次數多。一段代碼在其生命週期內會被讀取數百次,甚至數千次。
可讀性是能夠理解程序正在做什麼的關鍵,編寫可維護代碼的第一步是確保代碼可讀。
-
生產力: Go 程序員應該覺得他們可以通過 Go 語言完成很多工作。
快速編譯
是 Go 語言的一個關鍵特性,也是吸引新開發人員的關鍵工具,Go 語言編譯只需要幾秒鐘。Go 程序員相對於 C++ 來說不會花費整天的時間來調試不可思議的編譯錯誤。
三. 標識符
標識符是用來表示名稱的單詞,例如變量名稱、函數名稱、方法名稱、類型名稱、包名稱等等。
可讀性是良好代碼的定義質量,因此選擇好名稱對於 Go 代碼的可讀性至關重要。
- 標識符命名應該清晰而不是簡潔
-
好的命名應該簡潔: 好的名字不一定是最短的名字,但好的名字不會浪費在無關的東西上。
-
好的命名應該具備描述性: 好的命名會描述變量或常量的應用,而不是它們的內容。好的命名應該描述函數的結果或方法的行爲,而不是它們的操作。好的命名應該描述包的目的而非它的內容。
-
好的命名應該是高可讀性: 好的命名應該是具有高可讀性,能夠從名字中推斷出使用方式。
- 標識符長度有以下要求
- 短變量名稱在聲明和上次使用之間的距離很短時效果很好。短變量 p,我們只需要看 2 行代碼就可以知道它的用途。
1 var p = 0
2 p += count
3
4
- 長變量名稱需要證明自己的合理性,名稱越長需要提供的價值越高。
userCount
長變量一眼就可以知道是用於表示有多人用戶
1 var userCount int
2
3
-
請勿在變量名稱中包含類型名稱。例如不應該這樣定義
var usersMap map[string]int
,這個時候後綴的 Map 可以省略直接定義成這樣var users map[string]int
。 -
常量應該描述它們持有的值,而不是該如何使用。
-
對於循環和分支使用單字母變量,參數和返回值使用單個詞,函數聲明使用多個單詞,包的聲明使用單個詞。
1 # 循環
2 for i, v := range items {}
3 # 函數、參數和返回值
4 func getUserCount(endTime int64) (int64) {}
5 # 包
6 package http
7
8
- 包的名稱是調用者用來引用名稱的一部分,因此要好好利用這一點。
- 不要用變量類型命名你的變量
-
變量的名稱應描述其內容,而不是內容的類型。例如
var usersMap map[string]*User
應該命名成var users map[string]*User
。 -
對於函數來說也是適用的
func WriteConfig(w io.Writer, config *Config)
,命名參數*Config
爲config
是多餘的,因爲我們知道它的Config
類型所以使用conf
或c
都可以,如果有多個*Config
參數可以考慮使用origin
和update
。
- 使用一致的命名方式
-
例如代碼在處理數據庫請確保每次出現參數時,它都具有相同的名稱,例如統一使用
db *sql.DB
, 如果你看到db
你知道它就是*sql.DB
。 -
對於方法接收器, 在該類型的每個方法上使用相同的接收者名稱,例如結構體
type User struct
所有的方法接收器統一命名爲u *User
會更好理解。 -
有些變量是所有語言都默認會使用的,例如
i
,j
通常用於 for 循環的變量,n
通常用於計數器或累加器,v
通常用於值的簡寫,k
通常也用於 map 的鍵,s
通常用於字符串的簡寫。
- 使用一致的聲明樣式
- 聲明變量但沒有初始化時,請使用
var
,var
表示此變量已被聲明爲指定類型的零值。
1 var players int 默認爲0
2 var things []*Thing 默認爲nil
3
4
- 在同時聲明和初始化變量的時候,使用
:=
。
1 players := 5
2 things := make([]*Thing, 0)
3
4
- 成爲團隊合作者,所有代碼都需要使用 gofmt 格式化
四. 註釋
註釋對Go
語言程序的可讀性非常重要,註釋目的是如下 3 個。
-
註釋應該解釋其作用。
-
註釋應該解釋其如何做的。
-
註釋應該解釋其原因。
註釋的方式有以下幾種
- 文件註釋應該使用
// Package xxxx
開頭並且註釋是誰在什麼時間點創建或更新
1// Package main ...
2// Created by chenguolin 2018-12-26
3
4
- 公共變量、常量、可見函數註釋應該使用
//
1 // AppName application name
2 const AppName = "HTTP_SERVER"
3
4 // Users user count
5 var Users map[string]int
6
7 // GetUserName get user name by id
8 func GetUserName(id int64) string {}
9
10
註釋應該滿足以下幾點要求
- 關於變量和常量的註釋應描述其內容而非其目的
- 向變量或常量添加註釋時,該註釋應描述變量內容,而不是變量目的。
1 // randomNumber determined from an unbiased die
2 const randomNumber = 6
3
4
- 對於沒有初始值的變量,註釋應描述誰負責初始化此變量。
1 // sizeCalculationDisabled indicates whether it is safe
2 // to calculate Types' widths and alignments. See dowidth.
3 var sizeCalculationDisabled bool
4
5
- 公共符號始終要註釋
-
應該始終爲包中聲明的每個公共符號變量、常量、函數以及方法添加註釋。
-
任何既不明顯也不簡短的公共功能必須予以註釋。
-
無論長度或複雜程度如何,對庫中的任何函數都必須進行註釋。
-
在編寫函數之前,請編寫描述函數的註釋。如果你發現很難寫出註釋,那麼這就表明你將要編寫的代碼很難理解。
- 不要註釋不好的代碼,將它重寫
- 如果遇到不好的代碼,我們應提出問題,以提醒您稍後重構。標準庫中的慣例是注意到它的人用
TODO(username)
的樣式來註釋。
- 與其註釋一段代碼,不如重構它
- 好的代碼是最好的文檔,如果需要些很多的註釋來描述,不如重構這個代碼。
五. 包的設計
一個好的Go
語言包應該具有低程度的源碼級耦合,這樣隨着項目的增長,對一個包的更改不會跨代碼庫級聯。
- 一個好的包從它的名字開始
-
好的包名稱應該是唯一的,如果你發現有兩個包需要用相同名稱那麼它可能是太通用了或者與其它包名重疊了,這個時候我們應該考慮修改包名
-
避免使用類似
base
,common
或util
的包名稱,由於這些包包含各種不相關的功能,因此很難根據包提供的內容來描述它們。像utils
或helper
這樣的包名稱通常出現在較大的項目中,這些項目已經開發了深層次包的結構。 -
使用複數形式命名基礎包,例如
strings
來處理字符串。
- 儘早 return 而不是深度嵌套
- 由於 Go 語言的控制流不使用 exception,因此不需要爲 try 和 catch 塊提供頂級結構而深度縮進代碼。所以 Go 建議儘快 return 而不是深度嵌套。
- 讓零值更有用
-
假設變量沒有初始化,每個變量聲明都會自動初始化爲與零內存的內容相匹配的值。值的類型決定了其零值,對於數字類型它爲
0
,對於指針類型爲nil
, slices、 map 和 channel 同樣是nil
。 -
始終設置變量爲已知默認值的屬性對於程序的安全性和正確性非常重要,並且可以使 Go 語言程序更簡單、更緊湊。
六. 項目結構
通常一個項目是一個 git 倉庫,每個項目都應該有一個明確的目的。您應該避免在一個包實現多個目的,這將有助於避免成爲公共庫。
以httpserver
爲例,看通用的 HTTP service 項目的結構如下。
1httpserver
2├──cmd
3 ├──api
4 ├──cron
5 ├──processor
6├──common
7 ├──base62
8 ...
9├──config
10 ...
11├──context
12 ...
13├──docs
14 ...
15├──pkg
16 ...
17├──scripts
18 ...
19├──vendor
20 ...
21├──.gitlab-ci.yml
22├──Gopkg.lock
23├──Gopkg.toml
24├──README.md
25├──VERSION
26
27
- 考慮更少,更大的包
-
如果只有一個文件,文件名應該和文件夾名稱一樣。
-
如果有多個文件,應該按照不同的職責拆分爲不同的文件,不同的文件應該負責包的不同區域。
Go 編譯器並行編譯每個包,在一個包中編譯器並行編譯每個函數,更改包中代碼的佈局不會影響編譯時間。
- 優先內部測試再到外部測試
-
假設你的包名爲
http2
,您可以編寫http2_test.go
文件並使用包http2
聲明。這樣做會編譯http2_test.go
中的代碼,就像它是http2
包的一部分一樣,這就是內部測試
。如果http2_test.go
文件並使用包http2_test
聲明,則成爲外部測試
。 -
編寫單元測試時使用內部測試,這樣你就可以直接測試每個函數或方法,避免外部測試干擾。
-
如果有
example
測試代碼放在外部測試文件中。
- 確保 main 包內容儘可能的少
-
main
函數和main
包的內容應儘可能少, 這是因爲程序中只能有一個 main 函數。 -
應該將所有業務邏輯從
main
函數中移出,最好是從main
包中移出。 -
main
應該做解析flags
,開啓數據庫連接、開啓日誌等,然後將執行交給更高一級的對象。
七. 函數設計
函數設計非常的重要,如果函數沒有設計好那會導致不可兼容的情況,導致程序維護性變差。
- 設計難以被誤用的 函數
- 警惕採用幾個相同類型參數的函數,可能的解決方案是引入一個
helper
類型,它會負責如何正確地調用。
1 // 錯誤舉例
2 func CopyFile(src, dest string) error {}
3
4 CopyFile("file1", "file2")
5 CopyFile("file2", "file1")
6 對於上面的函數調用,如果參數填錯會導致文件被意外copy
7
8 // 準確舉例
9 type Source string
10 func (s *Source) Copy(dest string) error{}
11
12 file1.Copy(file2)
13 file2.Copy(file1)
14 上述這種使用方式則比較不容易出錯
15
16
- 不鼓勵使用 nil 作爲參數
- 不要在同一個函數簽名中混合使用可爲
nil
和不能爲nil
的參數
- 首選可變參數函數而非 []T 參數
- 例如
func ShutdownVMs(ids []string) error
但是很多時候這些類型的函數只用一個參數調用,爲了滿足函數參數的要求,它必須打包到一個切片內。這會增加額外的測試負載,因爲你應該涵蓋這些情況在測試中。
-
讓函數定義它們所需的行爲
-
空行來分解函數,讓代碼看起來更有層次感
八. 錯誤處理
- 通過消除錯誤來消除錯誤處理
- 當遇到難以忍受的錯誤處理時,請嘗試將某些操作提取到輔助程序類型中。
- 錯誤只處理一次
-
錯誤要麼在當前位置處理,要麼就返回給上層調用,不可同時處理否則就會導致重複的日誌輸出。
-
爲錯誤添加相關內容
九. 併發
Go 語言以channel
以及select和go
語句來支持併發。如果Go語言程序的main函數返回,無論程序在一段時間內啓動的其他goroutine在做什麼, Go語言程序會無條件地退出。
-
如果你的
goroutine
在得到另一個結果之前無法取得進展,那麼讓自己完成此工作而不是委託給其他goroutine
會更簡單。 -
將併發性留給調用者
- 如果你的函數啓動了
goroutine
,你必須爲調用者提供一種明確停止goroutine
的方法。把異步執行函數的決定留給該函數的調用者通常會更容易些。
- 永遠不要啓動一個停止不了的 goroutine
- 應用程序應該將監視其狀態和檢測是否重啓的工作留給另外的程序來做。不要讓你的應用程序負責重新啓動自己,最好從應用程序外部處理該過程。
-
只在 main.main 或 init 函數中的使用 panic
-
一旦我們開啓的所有
goroutine
都停止了, main.main 就會返回並且進程會乾淨地停止 -
每個 Goroutine 都需要有 Recover 機制,除非你允許程序 Crash
十. 工具
使用任何語言開發程序都需要有一個統一的規範,否則很容易造成風格不統一導致代碼混亂不可維護。使用 Golang 開發有幾個高效率工具
-
gofmt
Golang 的開發團隊制定了統一的官方代碼風格,並且推出了 gofmt 工具(gofmt 或 go fmt)來幫助開發者格式化他們的代碼到統一的風格。gofmt 是一個 cli 程序,會優先讀取標準輸入,如果傳入了文件路徑的話,會格式化這個文件,如果傳入一個目錄,會格式化目錄中所有. go 文件,如果不傳參數,會格式化當前目錄下的所有. go 文件。go fmt 命令,go fmt 命令是 gofmt 的簡單封裝。
常用的命令爲go list ./... | xargs -n 1 gofmt -l -w
-
go vet
go vet 就是 golang 中提供的語法檢查工具, 可以讓我檢查出 package 或者源碼文件中一些隱含的錯誤,規範我們的項目代碼。
常用的命令爲go vet ./...
-
golint
golint 是用來檢測 Golang 代碼規範的,不同於 gofmt 和 go vet。golint 只是用於代碼規範檢查
常用的命令爲go list ./... | xargs -n 1 golint
轉自:陳國林
鏈接:https://blog.csdn.net/chenguolinblog/article/details/90665174
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/DOSj9aVvtQGAnjmUEo7LyA