目录
序言
看过很多方面的编码规范,可能每一家公司都有不同的规范,这份编码规范是写给我自己的,同时希望我们公司内部同事也能遵循这个规范来写Go代码。
如果你的代码没有办法找到下面的规范,那么就遵循标准库的规范,多阅读标准库的源码,标准库的代码可以说是我们写代码参考的标杆。
本文中凡是【】内为规则的都是参考的标准库的源码,和【】内为原则的一样都是必须遵守的,【】内为建议的,是仅做建议遵守的。
本文参考了网上流传的众多开发规范帖子和标准款的规范代码,最后梳理而成,因内容过多分为几大篇,可选看,最后感谢那些原帖作者,文末给出了参考帖子的链接
目录
- 统一规范篇
- 命名篇
- 开发篇
- 优化篇
统一规范篇
本篇主要描述了公司内部同事都必须遵守的一些开发规矩,如统一开发空间,既使用统一的开发工具来保证代码最后的格式的统一,开发中对文件和代码长度的控制,必须经过go语言自带的检测机制等。
1.1 合理规划目录
【原则1.1】合理规划目录,一个目录中只包含一个包(实现一个模块的功能),如果模块功能复杂考虑拆分子模块,或者拆分目录。
说明:在Go中对于模块的划分是基于package这个概念,可以在一个目录中可以实现多个package,但是并不建议这样的实现方式。主要的缺点是模块之间的关系不清晰,另外不利于模块功能扩展。
错误示例:
project│ config.go│ controller.go│ filter.go│ flash.go│ log.go│ memzipfile.go│ mime.go│ namespace.go│ parser.go│ router.go│ staticfile.go│ template.go│ templatefunc.go│ tree.go│ util.go| validation.go| validators.go
推荐做法:
project ├─cache │ │ cache.go │ │ conv.go │ │ │ └─redis │ redis.go ├─config │ │ config.go │ │ fake.go │ │ ini.go │ └─yaml │ yaml.go ├─logs │ conn.go │ console.go │ file.go │ log.go │ smtp.go └─validation util.go validation.go validators.go
1.2 GOPATH设置
【建议1.2】使用单一的 GOPATH
虽说Go语言支持拥有多个 GOPATH,但多个GOPATH的情况并不具有弹性。GOPATH本身就是高度自我完备的(通过导入路径)。有多个 GOPATH 会导致某些副作用,例如可能使用了给定的库的不同的版本。你可能在某个地方升级了它,但是其他地方却没有升级。而且,我还没遇到过任何一个需要使用多个 GOPATH 的情况。所以只使用单一的 GOPATH,这会提升你 Go 的开发进度。
许多人不同意这一观点,接下来我会做一些澄清。像 etcd 或 camlistore 这样的大项目使用了像 godep 这样的工具,将所有依赖保存到某个目录中。也就是说,这些项目自身有一个单一的 GOPATH。它们只能在这个目录里找到对应的版本。除非你的项目很大并且极为重要,否则不要为每个项目使用不同的 GOPAHT。如果你认为项目需要一个自己的 GOPATH 目录,那么就创建它,否则不要尝试使用多个 GOPATH。它只会拖慢你的进度。
所有项目共用一个workspace,如下图所示:
workspace/ ├── bin ├── pkg │ └── linux_amd64 │ └── src ├── project1 │ └── project2 │ └── project3 │ └── …
优点: 方便发布到github.com, 让第三方通过go get等工具获取。
内部项目,建议采用第一种工程结构。公开项目、提供给第三方集成的项目采用第二种项目结构。
1.3 import 规范
import路径是一个唯一标示的字符串
import在多行的情况下,goimports会自动帮你格式化,但是我们这里还是规范一下import的一些规范,如果你在一个文件里面引入了一个package,还是建议采用如下格式:
import ( "fmt")
如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:
import ( "encoding/json" "strings" "myproject/models" "myproject/controller" "myproject/utils" "github.com/astaxie/beego" "github.com/go-sql-driver/mysql")
有顺序的引入包,不同的类型采用空格分离,第一种实标准库,第二是项目包,第三是第三方包。
【规则1.3.1】在非测试文件(*_test.go)中,禁止使用 . 来简化导入包的对象调用。
错误示例:
// 这是不好的导入 import . " pubcode/api/broker"
这种写法不利于阅读,因而不提倡。
【规则1.3.2】禁止使用相对路径导入(./subpackage),所有导入路径必须符合 go get 标准。
错误示例:
// 这是不好的导入 import "../net"
正确做法:
// 这是正确的做法 import "github.com/repo/proj/src/net"
【建议1.3.3】建议使用goimports工具或者IDE工具来管理多行import
go默认已经有了gofmt工具,但是我们强烈建议使用goimport工具,这个在gofmt的基础上增加了自动删除和引入包.
go get golang.org/x/tools/cmd/goimports
不同的编辑器有不同的配置, sublime的配置教程:
LiteIDE和GoLand默认已经支持了goimports,如果你的不支持请点击属性配置->golangfmt->勾选goimports
保存之前自动fmt你的代码。
好处:import在多行的情况下,goimports工具会自动帮你格式化,自动删除和引入包。很多IDE工具也可以自动检查并纠正import路径
1.4 代码风格
Go语言对代码风格作了很多强制的要求,并提供了工具gofmt, golint, go tool vet等工具检查。
【规则1.4.1】提交代码时,必须使用gofmt对代码进行格式化。
大部分的格式问题可以通过 gofmt 来解决,gofmt 自动格式化代码,保证所有的 go 代码与官方推荐的格式保持一致,所有格式有关问题,都以gofmt的结果为准。所以,建议在提交代码库之前先运行一下这个命令。
gofmt(也可以用go fmt,其操作于程序包的级别,而不是源文件级别),读入Go的源代码,然后输出按照标准风格缩进和垂直对齐的源码,并且保留了根据需要进行重新格式化的注释。如果你想知道如何处理某种新的布局情况,可以运行gofmt;如果结果看起来不正确,则需要重新组织你的程序,不要把问题绕过去。标准程序包中的所有Go代码,都已经使用gofmt进行了格式化。
不需要花费时间对结构体中每个域的注释进行排列,如下面的代码,
type T struct { name string // name of the object value int // its value }
gofmt将会按列进行排列:
type T struct { name string // name of the object value int // its value }
【规则1.4.2】提交代码时,必须使用golint对代码进行检查。
golint 会检测的方面:
- 变量名规范
- 变量的声明,像var str string = "test",会有警告,应该var str = "test"
- 大小写问题,大写导出包的要有注释
- x += 1 应该 x++
等等
详细可以看官方库示例,想速成的可以看自行学习使用
【建议1.4.3】提交代码前,必须使用go vet对代码进行检查。
如果说golint是检查我们的代码规范的话,那么vet工具则是可以帮我们静态分析我们的源码存在的各种问题,例如多余的代码,提前return的逻辑,struct的tag是否符合标准等。
go get golang.org/x/tools/cmd/vet
使用如下:
go vet .
1.5 大小约定
【建议1.5.1】单个文件长度不超过500行。
对开源引入代码可以降低约束,新增代码必须遵循。
【建议1.5.2】单个函数长度不超过50行。
函数两个要求:单一职责、要短小
【规则1.5.3】单个函数圈复杂度最好不要超过10,禁止超过15。
说明:圈复杂度越高,代码越复杂,就越难以测试和维护,同时也说明函数职责不单一。
【规则1.5.4】单行语句不能过长,如不能拆分需要分行写。一行最多120个字符。
换行时有如下建议:
换行时要增加一级缩进,使代码可读性更好; 低优先级操作符处划分新行;换行时操作符应保留在行尾; 换行时建议一个完整的语句放在一行,不要根据字符数断行示例:
if ((tempFlag == TestFlag) && (((counterVar - constTestBegin) % constTestModules) >= constTestThreshold)) { // process code }
【建议1.5.5】函数中缩进嵌套必须小于等于3层。
举例,禁止出现以下这种锯齿形的函数:
func testUpdateOpts PushUpdateOptions) (err error) { isNewRef := opts.OldCommitID == git.EMPTY_SHA isDelRef := opts.NewCommitID == git.EMPTY_SHA if isNewRef && isDelRef { if isDelRef { repo, err := GetRepositoryByName(owner.ID, opts.RepoName) if err != nil { if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) { if err := CommitRepoAction(CommitRepoActionOptions{ PusherName: opts.PusherName, RepoOwnerID: owner.ID, RepoName: repo.Name, RefFullName: opts.RefFullName, OldCommitID: opts.OldCommitID, NewCommitID: opts.NewCommitID, Commits: &PushCommits{}, }); err != nil { return fmt.Errorf("CommitRepoAction (tag): %v", err) } return nil } } else { owner, err := GetUserByName(opts.RepoUserName) if err != nil { return fmt.Errorf("GetUserByName: %v", err) } return nil } } } // other code }
提示:如果发现锯齿状函数,应通过尽早通过return等方法重构。
【原则1.5.6】保持函数内部实现的组织粒度是相近的。
举例,不应该出现如下函数:
func main() { initLog() //这一段代码的组织粒度,明显与其他的不均衡 orm.DefaultTimeLoc = time.UTC sqlDriver := beego.AppConfig.String("sqldriver") dataSource := beego.AppConfig.String("datasource") modelregister.InitDataBase(sqlDriver, dataSource) Run() }
应该改为:
func main() { initLog() initORM() //修改后,函数的组织粒度保持一致 Run() }