【原则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循环变量可以使用单字母。
有疑问加站长微信联系(非本文作者)