总述
对于团队而言,好的规范虽一定程度降低开发自由度,但带来的好处是不可忽视的
- 可以减少 bug 的产生
- 降低 review 和接手成本,通过统一规范,每个人代码风格统一,理想情况看谁的代码就像自己写的一样
- 利于写一些片段脚本
- 提高代码可读性
下面总结了平时项目中必须遵守的规范,后续会不断更新,建议收藏(除了“必须”级别的规范,如果感兴趣未来会更新“推荐”级别的规范)
代码风格
代码格式化
代码都必须格式化,也可以考虑使用 gofmt 或者 goimports 等工具,最好统一成一种,防止不同开发人员使用不同工具提交代码时有很多无意义的 diff
Import 规范
- 原则上遵循 goimports 规范,goimports 会自动把依赖按首字母排序,并对包进行分组管理,通过空行隔开,默认分为本地包(标准库、内部包)、第三方包。
- 标准包永远位于最上面的第一组
- 使用完整路径,不要使用相对路径
- 包名和 git 路径名不一致时,或者多个相同包名冲突时,使用别名代替
错误处理
- error 作为函数的值返回,必须对 error 进行处理, 或将返回值赋值给明确忽略。
- error 作为函数的值返回且有多个返回值的时候,error 必须是最后一个参数。
// 不建议
func do() (error, int) {
}
// 建议
func do() (int, error) {
}
- 优先处理错误,能 return 尽早 return。理想情况代码逻辑是平铺的顺着读就能看懂,过多的嵌套会降低可读性
// 不建议
if err != nil {
// error handling
} else {
// normal code
}
// 建议
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
- 错误返回优先独立判断,不与其他变量组合判断
// 不建议
x, y, err := f()
if err != nil || y == nil {
return err // 当y与err都为空时,函数的调用者会出现错误的调用逻辑
}
// 建议
x, y, err := f()
if err != nil {
return err
}
if y == nil {
return fmt.Errorf("some error")
}
panic
- 在业务逻辑处理中禁止使用 panic。
- 在 main 包中只有当完全不可运行的情况可使用 panic,例如:文件无法打开,数据库无法连接导致程序无法正常运行。
- 对于其它的包,可导出的接口不能有 panic,只能在包内使用。
- 建议在 main 包中使用 log.Fatal 来记录错误,这样就可以由 log 来结束程序,或者将 panic 抛出的异常记录到日志文件中,方便排查问题。
- panic 捕获只能到 goroutine 最顶层,每个自行启动的 goroutine,必须在入口处捕获 panic,并打印详细堆栈信息或进行其它处理。
recover
- recover 用于捕获 runtime 的异常,禁止滥用 recover。
- 必须在 defer 中使用,一般用来捕获程序运行期间发生异常抛出的 panic 或程序主动抛出的 panic。
单元测试
- 单元测试文件名命名规范为 example_test.go。
- 测试用例的函数名称必须以 Test 开头,例如 TestExample。
- 如果存在 func Foo,单测函数可以带下划线,为 func Test_Foo。如果存在 func (b *Bar) Foo,单测函数可以为 func TestBar_Foo。下划线不能出现在前面描述情况以外的位置。
- 每个重要的可导出函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试。
类型断言失败处理
type assertion 的单个返回值形式针对不正确的类型将产生 panic。因此,请始终使用 “comma ok” 的惯用法。
// 不建议
t := i.(string)
// 建议
t, ok := i.(string)
if !ok {
// 优雅地处理错误
}
注释
- 在编码阶段同步写好变量、函数、包注释,注释可以通过 godoc 导出生成文档。
- 程序中每一个被导出的(大写的)名字,都应该有一个文档注释。
- 所有注释掉的代码在提交 code review 前都应该被删除,除非添加注释讲解为什么不删除, 并且标明后续处理建议(比如删除计划)。
命名
- 文件名应该采用小写,并且使用下划线分割各个单词,文件名尽量采用有意义简短的文件
- 结构体,接口,变量,常量,函数均采用驼峰命名
控制结构
- if 语句
// 不建议,变量优先在左
if nil != err {
}
// 建议这种
if err != nil {
}
// 不建议,bool类型变量直接进行
if has == true {
}
// 建议
if has {
}
- switch 语句,必须有 default 哪怕什么都不做
- 业务代码禁止使用 goto,其他框架或底层源码推荐尽量不用。
函数
函数返回相同类型的两个或三个参数,或者如果从上下文中不清楚结果的含义,使用命名返回,其它情况不建议使用命名返回。
func (n *Node) Parent1() *Node
func (n *Node) Parent2() (*Node, error)
func (f *Foo) Location() (lat, long float64, err error)
Defer
- 当存在资源管理时,应紧跟 defer 函数进行资源的释放。
- 判断是否有错误发生之后,再 defer 释放资源。
resp, err := http.Get(url)
if err != nil {
return err
}
// defer 放到错误处理之后,不然可能导致panic
defer resp.Body.Close()
- 禁止在循环中的延迟函数中使用 defer,因为 defer 的执行需要外层函数的结束才会释放,未来会有很多坑
// 不要这样使用
func filterSomething(values []string) {
for _, v := range values {
fields, err := db.Query(xxx)
if err != nil {
}
defer fields.Close()
// xxx
}
}
// 但是可以使用这种方式
func filterSomething(values []string) {
for _, v := range values {
func() {
fields, err := db.Query(xxx)
if err != nil {
// xxx
}
defer fields.Close()
//x xxx
}()
}
}
魔法数字
如果魔法数字出现超过 2 次,则禁止使用。
func getArea(r float64) float64 {
return 3.14 * r * r
}
func getLength(r float64) float64 {
return 3.14 * 2 * r
}
// 建议定义一个常量代替魔法数字
// PI xxx
const PI = 3.14