【原則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循環變量可以使用單字母。