【原則2.1】合理規劃目錄,一個目錄中只包含一個包(實現一個模塊的功能),如果模塊功能複雜考慮拆分子模塊,或者拆分目錄。
說明:在Go中對於模塊的劃分是基於package這個概念,可以在一個目錄中可以實現多個package,但是並不建議這樣的實現方式。主要的缺點是模塊之間的關係不清晰,另外不利於模塊功能擴展。
錯誤示例:
1. project
2. │ config.go
3. │ controller.go
4. │ filter.go
5. │ flash.go
6. │ log.go
7. │ memzipfile.go
8. │ mime.go
9. │ namespace.go
10. │ parser.go
11. │ router.go
12. │ staticfile.go
13. │ template.go
14. │ templatefunc.go
15. │ tree.go
16. │ util.go
17. | validation.go
18. | validators.go
推薦做法:
1. project
2. ├─cache
3. │ │ cache.go
4. │ │ conv.go
5. │ │
6. │ └─redis
7. │ redis.go
8. ├─config
9. │ │ config.go
10. │ │ fake.go
11. │ │ ini.go
12. │ └─yaml
13. │ yaml.go
14. ├─logs
15. │ conn.go
16. │ console.go
17. │ file.go
18. │ log.go
19. │ smtp.go
20. └─validation
21. util.go
22. validation.go
23. validators.go
2.2 GOPATH設置
【建議2.1】內部項目GOPATH建議指向多個工作目錄。
Go語言有兩種工程模式:
一項目一個workspace
這種項目結構中,每一個工程有一個完整的workspace空間,互相隔離,go get命令默認會使用GOPATH中第1個workspace,優點:項目之間互相隔離。
所有項目共用一個workspace,如下圖所示:
workspace/
├── bin
├── pkg
│ └── linux_amd64
│
└── src
├── project1
│
└── project2
│
└── project3
│
└── …
優點: 方便發佈到github.com, 讓第三方通過go get等工具獲取。
內部項目,建議採用第一種工程結構。公開項目、提供給第三方集成的項目採用第二種項目結構。
2.3 import路徑
import路徑是一個唯一標示的字符串,下面是一個完整的示例:
1. import (
2. "errors"
3. "fmt"
4. "os"
5. "strings"
6. "sync"
7. "time"
8.
9. "github.com/fsnotify/fsnotify"
10. jww "github.com/spf13/jwalterweatherman"
11. )
【規則2.1】在非測試文件(*_test.go)中,禁止使用 . 來簡化導入包的對象調用。
錯誤示例:
1. // 這是不好的導入
2. import . " pubcode/api/broker"
這種寫法不利於閱讀,因而不提倡。
【規則2.2】禁止使用相對路徑導入(./subpackage),所有導入路徑必須符合 go get 標準。
錯誤示例:
1. // 這是不好的導入
2. import "../net"
正確做法:
1. // 這是正確的做法
2. import "github.com/repo/proj/src/net"
【建議2.2】建議使用goimports工具或者IDE工具來管理多行import
好處:import在多行的情況下,goimports工具會自動幫你格式化,自動刪除和引入包。很多IDE工具也可以自動檢查並糾正import路徑
【建議3.15】接收者名不要使用me,this 或者 self 這種泛指的名字。
【建議3.16】定義方法時,如果方法內不會直接引用接收者,則省略掉接收者名。
舉例:
1. func (T) sayHi() {
2. // do things without T
3. }
4.
5. func (*T) sayHello() {
6. // do things without *T
7. }
3.1.12 返回值
【規則3.14】返回值如果是命名的,則必須大小寫混排,首字母小寫。
【建議3.17】函數的返回值應避免使用命名的參數。
舉例:
1. func (n *Node) Bad() (node *Node, err error)
2. func (n *Node) Good() (*Node, error)
因爲如果使用命名變量很容易導致臨時變量覆蓋而引起隱藏的bug。
例外情況:多個返回值類型相同的情況下,使用命名返回值來區分不同的返回參數。
說明:命名返回值使代碼更清晰,同時更加容易讀懂。
舉例:
1. func getName()(firstName, lastName, nickName string){
2. firstName = "May"
3. lastName = "Chen"
4. nickName = "Babe"
5. return
6. }
參考:https://github.com/golang/go/wiki/CodeReviewComments#named-result-parameters
https://golang.org/doc/effective_go.html#named-results
【規則3.15】函數返回值個數不要超過3個。
【建議3.18】如果函數的返回值超過3個,建議將其中關係密切的返回值參數封裝成一個結構體。
3.1.13 魔鬼數字
【規則3.16】代碼中禁止使用魔鬼數字。
說明:直接使用數字,造成代碼難以理解,也難以維護。應採用有意義的靜態變量或枚舉來代替。
例外情況:有些特殊情況下,如循環或比較時採用數字0,-1,1,這些情況可採用數字。
3.2 代碼格式化要求
go默認已經有了gofmt工具,如果使用sublime、LiteIDE等goIDE工具,可以在IDE中自動格式化代碼。除此之外,還有一些規範是需要開發者自行遵守的。
【規則3.17】運算符前後、逗號後面、if後面等需有單空格隔開。
1) if err != nil {…}
2) c := a + b
3) return {}, err
例外情況:
go fmt認爲應該刪除空格的場景。例如,在傳參時,字符串拼接的”+”號。
【規則3.18】相對獨立的程序塊之間、變量說明之後必須加空行,而邏輯緊密相關的代碼則放在一起。
不好的例子:
1. func formatResponseBody(res *http.Response, httpreq *httplib.BeegoHttpRequest, pretty bool) string {
2. body, err := httpreq.Bytes()
3. if err != nil {
4. log.Fatalln("can't get the url", err)
5. }
6. match, err := regexp.MatchString(contentJsonRegex, res.Header.Get("Content-Type"))
7. if err != nil {
8. log.Fatalln("failed to compile regex", err)
9. }
10. if pretty && match {
11. var output bytes.Buffer
12. err := json.Indent(&output, body, "", " ")
13. if err != nil {
14. log.Fatal("Response Json Indent: ", err)
15. }
16. return output.String()
17. }
18. return string(body)
19. }
應該改爲:
1. func formatResponseBody(res *http.Response, httpreq *httplib.BeegoHttpRequest, pretty bool) string {
2. body, err := httpreq.Bytes()
3. if err != nil {
4. log.Fatalln("can't get the url", err)
5. }
6.
7. match, err := regexp.MatchString(contentJsonRegex, res.Header.Get("Content-Type"))
8. if err != nil {
9. log.Fatalln("failed to compile regex", err)
10. }
11.
12. if pretty && match {
13. var output bytes.Buffer
14. err := json.Indent(&output, body, "", " ")
15. if err != nil {
16. log.Fatal("Response Json Indent: ", err)
17. }
18.
19. return output.String()
20. }
21.
22. return string(body)
23. }
提示:當你需要爲接下來的代碼增加註釋的時候,說明該考慮加一行空行了。
【規則3.19】儘早return:一旦有錯誤發生,馬上返回。
舉例:不要使用
1. if err != nil {
2. // error handling
3. } else {
4. // normal code
5. }
而推薦使用:
1. if err != nil {
2. // error handling
3. return // or continue, etc.
4. }
5.
6. // normal code
這樣可以減少嵌套深度,代碼更加美觀。
【規則3.20】單行語句不能過長,如不能拆分需要分行寫。一行最多120個字符。
換行時有如下建議:
換行時要增加一級縮進,使代碼可讀性更好;
低優先級操作符處劃分新行;換行時操作符應保留在行尾;
換行時建議一個完整的語句放在一行,不要根據字符數斷行
示例:
1. if ((tempFlag == TestFlag) &&
2. (((counterVar - constTestBegin) % constTestModules) >= constTestThreshold)) {
3. // process code
4. }
【建議3.19】單個文件長度不超過500行。
對開源引入代碼可以降低約束,新增代碼必須遵循。
【建議3.20】單個函數長度不超過50行。
函數兩個要求:單一職責、要短小
【規則3.21】單個函數圈複雜度最好不要超過10,禁止超過15。
說明:圈複雜度越高,代碼越複雜,就越難以測試和維護,同時也說明函數職責不單一。
【建議3.21】函數中縮進嵌套必須小於等於3層。
舉例,禁止出現以下這種鋸齒形的函數:
1. func testUpdateOpts PushUpdateOptions) (err error) {
2. isNewRef := opts.OldCommitID == git.EMPTY_SHA
3. isDelRef := opts.NewCommitID == git.EMPTY_SHA
4. if isNewRef && isDelRef {
5. if isDelRef {
6. repo, err := GetRepositoryByName(owner.ID, opts.RepoName)
7. if err != nil {
8. if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
9. if err := CommitRepoAction(CommitRepoActionOptions{
10. PusherName: opts.PusherName,
11. RepoOwnerID: owner.ID,
12. RepoName: repo.Name,
13. RefFullName: opts.RefFullName,
14. OldCommitID: opts.OldCommitID,
15. NewCommitID: opts.NewCommitID,
16. Commits: &PushCommits{},
17. }); err != nil {
18. return fmt.Errorf("CommitRepoAction (tag): %v", err)
19. }
20. return nil
21. }
22. }
23. else {
24. owner, err := GetUserByName(opts.RepoUserName)
25. if err != nil {
26. return fmt.Errorf("GetUserByName: %v", err)
27. }
28.
29. return nil
30. }
31. }
32. }
33.
34. // other code
35. }
提示:如果發現鋸齒狀函數,應通過儘早通過return等方法重構。
【原則3.2】保持函數內部實現的組織粒度是相近的。
舉例,不應該出現如下函數:
1. func main() {
2. initLog()
3.
4. //這一段代碼的組織粒度,明顯與其他的不均衡
5. orm.DefaultTimeLoc = time.UTC
6. sqlDriver := beego.AppConfig.String("sqldriver")
7. dataSource := beego.AppConfig.String("datasource")
8. modelregister.InitDataBase(sqlDriver, dataSource)
9.
10. Run()
11. }
應該改爲:
1. func main() {
2. initLog()
3.
4. initORM() //修改後,函數的組織粒度保持一致
5.
6. Run()
7. }
3.1.5 結構體名
【規則3.8】結構體名必須爲大小寫混排的駝峯模式,不允許出現下劃線,可被包外部引用則首字母大寫;如僅包內使用,則首字母小寫。
例如:
1. type ServicePlan struct
2. type internalBroker struct
【建議3.7】結構名建議採用名詞、動名詞爲好。
3.1.6 常量與枚舉
常量&枚舉名,推薦採用大小寫混排的駝峯模式(Golang官方要求),不允許出現下劃線,如:
1. const (
2. CategoryBooks = iota // 0
3. CategoryHealth // 1
4. CategoryClothing // 2
5. )
只有從其他標準移植過來的常量才和原來保持一致,比如:
自定義的 http.StatusOK
移植過來的 tls.TLS_RSA_WITH_AES_128_CBC_SHA
按照功能來區分,而不是將所有類型都分在一組,並建議將公共常量置於私有常量之前:
1. const (
2. KindPage = "page"
3.
4. // The rest are node types; home page, sections etc.
5. KindHome = "home"
6. KindSection = "section"
7. KindTaxonomy = "taxonomy"
8. KindTaxonomyTerm = "taxonomyTerm"
9.
10. // Temporary state.
11. kindUnknown = "unknown"
12.
13. // The following are (currently) temporary nodes,
14. // i.e. nodes we create just to render in isolation.
15. kindRSS = "RSS"
16. kindSitemap = "sitemap"
17. kindRobotsTXT = "robotsTXT"
18. kind404 = "404"
19. )
如果是枚舉類型的常量,需要先創建相應類型:
1. type tstCompareType int
2.
3. const (
4. tstEq tstCompareType = iota
5. tstNe
6. tstGt
7. tstGe
8. tstLt
9. tstLe
10. )
如果模塊的功能較爲複雜、常量名稱容易混淆的情況下,爲了更好地區分枚舉類型,可以使用完整的前綴:
1. type PullRequestStatus int
2.
3. const (
4. PullRequestStatusConflict PullRequestStatus = iota
5. PullRequestStatusChecking
6. PullRequestStatusMergeable
7. )
3.1.7 參數名
【規則3.9】參數名必須爲大小寫混排,且首字母小寫,不能有下劃線。
例如:
1. func MakeRegexpArray(str string)
【建議3.8】參數按邏輯緊密程度安排位置, 同種類型的參數放在相鄰位置。
舉例:
1) func(m1, m2 *MenuEntry) bool
2) func (c *Client) Delete(key string, recursive bool, dir bool) (*RawResponse, error)
【建議3.9】避免使用標識參數來控制函數的執行邏輯。
舉例:
1. func doAorB(flag int) {
2. if flag == flagA {
3. processA1()
4. return
5. }
6.
7. if flag == flagB {
8. processB1()
9. return
10. }
11. }
特別是標識爲布爾值時,通過標識參數控制函數內的邏輯,true執行這部分邏輯,false執行另外一部分邏輯,說明了函數職責不單一。
【建議3.10】參數個數不要超過5個
參數過多通常意味着缺少封裝,不易維護,容易出錯.
3.1.8 全局變量名
【規則3.10】全局變量必須爲大小寫混排的駝峯模式,不允許出現下劃線。首字母根據作爲範圍確定大小寫。
例如:
1. var Global int //包外
2. var global int //包內
【建議3.11】儘量避免跨package使用全局變量,儘量減少全局變量的使用。
3.1.9 局部變量名
【規則3.11】局部變量名必須爲大小寫混排,且首字母小寫,不能有下劃線。
例如:
1. result, err := MakeRegexpArray(str)
【建議3.12】for循環變量可以使用單字母。
3.1.10 接口名
【規則3.12】接口名必須爲大小寫混排,支持包外引用則首字母大寫,僅包內使用則首字母小寫。不能有下劃線,整體必須爲名詞。
【建議3.13】最好以“er”結尾,除非有更合適的單詞。
例如:
1. type Reader interface {...}
3.1.11 方法接收者名
【規則3.13】方法接收名必須爲大小寫混排,首字母小寫。方法接收者命名要能夠體現接收者對象。
【建議3.14】接收者名通常1個或者2個字母就夠,最長不能超過4個字母。
例如:
1. func (c *Controller) Run(stopCh <-chan struct{})
參考:https://github.com/golang/go/wiki/CodeReviewComments#receiver-names
2.4 第三方包管理
【建議2.3】項目倉庫中包含全量的代碼
說明:將依賴源碼都放到當前工程的vendor目錄下,將全量的代碼保存到項目倉庫中,這樣做有利於避免受第三方變動的影響。
【建議2.4】建議採用 Glide 來管理第三方包
第三方包應該儘量獲取release版本,而非master分支的版本。master上的版本通常是正在開發的非穩定版本
3 代碼風格
Go語言對代碼風格作了很多強制的要求,並提供了工具gofmt, golint, go tool vet, errcheck等工具檢查。
【規則3.1】提交代碼時,必須使用gofmt對代碼進行格式化。
【規則3.2】提交代碼時,必須使用golint對代碼進行檢查。
【建議3.1】在代碼中編寫字符串形式的json時,使用反單引號,而不是雙引號。
例如:
"{\"key\":\"value\"}"
改爲格式更清晰的:
`
{
"key":"value"
}
`
gofmt(也可以用go fmt,其操作於程序包的級別,而不是源文件級別),讀入Go的源代碼,然後輸出按照標準風格縮進和垂直對齊的源碼,並且保留了根據需要進行重新格式化的註釋。如果你想知道如何處理某種新的佈局情況,可以運行gofmt;如果結果看起來不正確,則需要重新組織你的程序,不要把問題繞過去。標準程序包中的所有Go代碼,都已經使用gofmt進行了格式化。
不需要花費時間對結構體中每個域的註釋進行排列,如下面的代碼,
1. type T struct {
2. name string // name of the object
3. value int // its value
4. }
gofmt將會按列進行排列:
1. type T struct {
2. name string // name of the object
3. value int // its value
4. }
3.1 命名
3.1.1 文件名
和其它語言一樣,名字在Go中是非常重要的。它們甚至還具有語義的效果:一個名字在程序包之外的可見性是由它的首字符是否爲大寫來確定的。因此,值得花費一些時間來討論Go程序中的命名約定。
【規則3.3】文件名必須爲小寫單詞,允許加下劃線‘_’組合方式,但是頭尾不能爲下劃線。
例如: port_allocator.go
【建議3.2】雖然允許出現下劃線,但是儘量避免。
如果採用下劃線的方式,注意避免跟下面保留特定用法的後綴衝突:
1)測試文件:_test.go
2)系統相關的文件:_386.go、_amd64.go、_arm.go、_arm64.go、_android.go、_darwin.go、_dragonfly.go、_freebsd.go、_linux.go、_nacl.go、_netbsd.go、_openbsd.go、_plan9.go、_solaris.go、_windows.go、_android_386.go、_android_amd64.go、_android_arm.go、_android_arm64.go、_darwin_386.go、_darwin_amd64.go、_darwin_arm.go、_darwin_arm64.go、_dragonfly_amd64.go、_freebsd_386.go、_freebsd_amd64.go、_freebsd_arm.go、_linux_386.go、_linux_amd64.go、_linux_arm.go、_linux_arm64.go、_linux_mips64.go、_linux_mips64le.go、_linux_ppc64.go、_linux_ppc64le.go、_linux_s390x.go、_nacl_386.go、_nacl_amd64p32.go、_nacl_arm.go、_netbsd_386.go、_netbsd_amd64.go、_netbsd_arm.go、_openbsd_386.go、_openbsd_amd64.go、_openbsd_arm.go、_plan9_386.go、_plan9_amd64.go、_plan9_arm.go、_solaris_amd64.go、_windows_386.go
_windows_amd64.go
【建議3.3】文件名以功能爲指引,名字中不需再出現模塊名或者組件名。
因爲Go包的導入是與路徑有關的,本身已經隱含了模塊/組件信息。
3.1.2 目錄名
【規則3.4】目錄名必須爲全小寫單詞,允許加中劃線‘-’組合方式,但是頭尾不能爲中劃線。
例如:
go-sql-driver
hsa-microservice
service-mgr
【建議3.4】雖然允許出現中劃線,但是儘量避免或少加中劃線。
3.1.3 包名
【原則3.1】取名儘量簡單和可閱讀。
【規則3.5】包名必須全部爲小寫單詞,無下劃線,越短越好。儘量不要與標準庫重名。
原因:包名在被導入後,會以 package.Func()方式使用,任何人使用你的包都得敲一遍該包名,如:
io/ioutil,不要用 io/util
suffixarray,不要用 suffix_array
包名也是類型和函數的一部分,比如:
buf := new(bytes.Buffer)
就不要取名爲 bytes.BytesBuffer,過於累贅。
【規則3.6】禁止通過中劃線連接多個單詞的方式來命名包名。
package go-oci8 //編譯錯誤
【建議3.5】包名儘量與所在目錄名一致,引用時比較方便。
這是因爲在import導入的包是按目錄名來命名的,如果不一致,代碼閱讀者就很困惑。
3.1.4 函數名/方法名
【規則3.7】函數名必須爲大小寫混排的駝峯模式,不允許出現下劃線。
【建議3.6】函數名力求精簡準確,並採用用動詞或者動賓結構的單詞
例如:
1. func MakeRegexpArrayOrDie // 暴露給包外部函數
2. func matchesRegexp // 包內部函數
3.1.5 結構體名
【規則3.8】結構體名必須爲大小寫混排的駝峯模式,不允許出現下劃線,可被包外部引用則首字母大寫;如僅包內使用,則首字母小寫。
例如:
1. type ServicePlan struct
2. type internalBroker struct
【建議3.7】結構名建議採用名詞、動名詞爲好。
3.1.6 常量與枚舉
常量&枚舉名,推薦採用大小寫混排的駝峯模式(Golang官方要求),不允許出現下劃線,如:
1. const (
2. CategoryBooks = iota // 0
3. CategoryHealth // 1
4. CategoryClothing // 2
5. )
只有從其他標準移植過來的常量才和原來保持一致,比如:
自定義的 http.StatusOK
移植過來的 tls.TLS_RSA_WITH_AES_128_CBC_SHA
按照功能來區分,而不是將所有類型都分在一組,並建議將公共常量置於私有常量之前:
1. const (
2. KindPage = "page"
3.
4. // The rest are node types; home page, sections etc.
5. KindHome = "home"
6. KindSection = "section"
7. KindTaxonomy = "taxonomy"
8. KindTaxonomyTerm = "taxonomyTerm"
9.
10. // Temporary state.
11. kindUnknown = "unknown"
12.
13. // The following are (currently) temporary nodes,
14. // i.e. nodes we create just to render in isolation.
15. kindRSS = "RSS"
16. kindSitemap = "sitemap"
17. kindRobotsTXT = "robotsTXT"
18. kind404 = "404"
19. )
如果是枚舉類型的常量,需要先創建相應類型:
1. type tstCompareType int
2.
3. const (
4. tstEq tstCompareType = iota
5. tstNe
6. tstGt
7. tstGe
8. tstLt
9. tstLe
10. )
如果模塊的功能較爲複雜、常量名稱容易混淆的情況下,爲了更好地區分枚舉類型,可以使用完整的前綴:
1. type PullRequestStatus int
2.
3. const (
4. PullRequestStatusConflict PullRequestStatus = iota
5. PullRequestStatusChecking
6. PullRequestStatusMergeable
7. )
3.1.7 參數名
【規則3.9】參數名必須爲大小寫混排,且首字母小寫,不能有下劃線。
例如:
1. func MakeRegexpArray(str string)
【建議3.8】參數按邏輯緊密程度安排位置, 同種類型的參數放在相鄰位置。
舉例:
1) func(m1, m2 *MenuEntry) bool
2) func (c *Client) Delete(key string, recursive bool, dir bool) (*RawResponse, error)
【建議3.9】避免使用標識參數來控制函數的執行邏輯。
舉例:
1. func doAorB(flag int) {
2. if flag == flagA {
3. processA1()
4. return
5. }
6.
7. if flag == flagB {
8. processB1()
9. return
10. }
11. }
特別是標識爲布爾值時,通過標識參數控制函數內的邏輯,true執行這部分邏輯,false執行另外一部分邏輯,說明了函數職責不單一。
【建議3.10】參數個數不要超過5個
參數過多通常意味着缺少封裝,不易維護,容易出錯.
3.1.8 全局變量名
【規則3.10】全局變量必須爲大小寫混排的駝峯模式,不允許出現下劃線。首字母根據作爲範圍確定大小寫。
例如:
1. var Global int //包外
2. var global int //包內
【建議3.11】儘量避免跨package使用全局變量,儘量減少全局變量的使用。
3.1.9 局部變量名
【規則3.11】局部變量名必須爲大小寫混排,且首字母小寫,不能有下劃線。
例如:
1. result, err := MakeRegexpArray(str)
【建議3.12】for循環變量可以使用單字母。