看过很多方面的编码规范,可能每一家公司都有不同的规范,这份编码规范是写给我自己的,同时希望我们公司内部同事也能遵循这个规范来写Go代码。
如果你的代码没有办法找到下面的规范,那么就遵循标准库的规范,多阅读标准库的源码,标准库的代码可以说是我们写代码参考的标杆。
本文中凡是【】内为规则的都是参考的标准库的源码,和【】内为原则的一样都是必须遵守的,【】内为建议的,是仅做建议遵守的。
本文参考了网上流传的众多开发规范帖子和标准款的规范代码,最后梳理而成,因内容过多分为几大篇,可选看,最后感谢那些原帖作者,文末给出了参考帖子的链接
本篇主要描述了公司内部同事都必须遵守的一些开发规矩,如统一开发空间,既使用统一的开发工具来保证代码最后的格式的统一,开发中对文件和代码长度的控制,必须经过go语言自带的检测机制等。
【原则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
虽说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等工具获取。
内部项目,建议采用第一种工程结构。公开项目、提供给第三方集成的项目采用第二种项目结构。
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的配置教程:http://michaelwhatcott.com/gosublime-goimports/
LiteIDE和GoLand默认已经支持了goimports,如果你的不支持请点击属性配置->golangfmt->勾选goimports
保存之前自动fmt你的代码。
好处:import在多行的情况下,goimports工具会自动帮你格式化,自动删除和引入包。很多IDE工具也可以自动检查并纠正import路径
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 会检测的方面:
等等
详细可以看官方库示例,https://github.com/golang/lint/tree/master/testdata
想速成的可以看Golang lint简易使用方法自行学习使用
【建议1.4.3】提交代码前,必须使用go vet对代码进行检查。
如果说golint是检查我们的代码规范的话,那么vet工具则是可以帮我们静态分析我们的源码存在的各种问题,例如多余的代码,提前return的逻辑,struct的tag是否符合标准等。
go get golang.org/x/tools/cmd/vet
使用如下:
go vet .
【建议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()
}
https://studygolang.com/articles/2059
https://studygolang.com/articles/12033
https://blog.csdn.net/shuanger_/article/details/48241767
https://blog.csdn.net/tanzhe2017/article/list