​​总述

对于团队而言,好的规范虽一定程度降低开发自由度,但带来的好处是不可忽视的

  • 可以减少 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