go编码规范 | 青训营笔记

117 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 12 天

今天和大家分享高性能编码规范的相关知识点

高质量编码规范

编写质量高,易于阅读,易于维护的代码在工作中十分重要,这里列出了一些比较公认的编码规范。

注释

什么代码需要注释?

  • 公共符号注释:公共符号一定要注释,包中声明的每个公共符号:变量,常量,函数以及结构体都需要添加注释。
  • 长代码块注释:任何既不明显也不简短的公共功能必须予以注释
  • 库函数注释:无论长度和复杂度如何,对库中的任何函数都必须进行注释
  • 例外:不需要注释实现接口的方法。例如 func (r *FileReader) Read(Buf []byte) (int, error) 因为该接口的使用注释应该在接口定义处已经写明了。

注释应该说明什么?

  • 注释应该解释代码的作用

  • 解释代码什么情况下会出错,出什么错

      // Mul 实现a/b的出发功能,当b0时返回错误
      func Mul(a, b int) (err error) {
          ...
      }
    
  • 合适注释复杂代码块的实现过程

      // 加入凭证配置和指定加密算法,并根据自定义key生成token字符串
      c := &UserClaims{
      Name:   "aei",
      Expire: int(604800 + time.Now().Unix()), // 有效期7天
      }
      claim := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
      str, err := claim.SignedString([]byte(key))
    

    当然,如果一个函数名或者代码块已经很清晰的描述了其作用和使用方法,其实就没必要再加注释了:

       func isNumber(num interface{}) bool 
    
  • 公共符号需要注释说明:

    // A JWT Token.  Different fields will be used depending on whether you're
    // creating or parsing/verifying a token.
    type Token struct {
        Raw       string                 // The raw token.  Populated when you Parse a token
        Method    SigningMethod          // The signing method used or to be used
        Header    map[string]interface{} // The first segment of the token
        Claims    Claims                 // The second segment of the token
        Signature string                 // The third segment of the token.  Populated when you Parse a token
        Valid     bool                   // Is the token valid?  Populated when you Parse/Verify a token
    }
    
  • 代码时最好的注释,代码清晰,命名清晰最重要,其次才是通过注释提供代码未明确表达出来的信息

命名规范

  • 变量应该有意义,比如截止时间变量,我们使用 deadline 比使用 t 更加清晰有效;
  • 函数名应该具体简洁,不需要带上下文信息。比如 service 层的获取用户信息函数,使用 UserInfo 比使用 ServiceUserInfo 更加简洁;
  • 包名只由小写字母组成,不包含大写字母和下划线等字符,应该简短并包含一定的上下文信息,并且不使用常用变量和标准库的名字,例如使用 userservice 而不是 user

流程控制

流程控制中出现多个分支时,我们应该避免嵌套和冗余。例如:

// 不优雅的写法
if foo {
    return x
} else {
    return err
}
// 优雅写法
if foo {
    return x
}
return nil

当分支比较多或者代码比较复杂时,我们应该尽早处理特殊情况或者错误情况,来尽早返回减少嵌套

// 不优雅的写法
func Func() error {
    err := doSomething()
    if err == nil {
        err := doAnotherThing()
        if err == nil {
            return nil
        }
        return err
    }
    return err
}
// 优雅的写法
func Func() error {
    if err := doSomething(); err != nil {
        return err
    }
    if err := doAnotherThing(); err != nil {
        return err
    }
    return nil
}

错误处理

简单错误

对于简单的自定义错误,我们可以直接使用errors.New返回

return errors.New("to much fish")

复杂错误(实现错误跟踪链)

error

复杂的错误我们可以使用错误包装和解包。错误的包装实际上提供了一个error嵌套另一个error的能力,从而在解包时形成一个跟踪链。

fmt.Errorf 中使用 %w 关键字来进行包装

var ErrDemo = errors.New("123")
​
func main() {
    err1 := ErrDemo
    msg := "from main"
    err2 := fmt.Errorf("error: %v %w", msg, ErrDemo)
    err3 := fmt.Errorf("error: %w %v", err2, "又一个错误")
    // errors.Is 时会自动解包,当遇到 %w 包装的 错误时,会自动找出其中的错误并解包
    fmt.Println(err1, errors.Is(err1, ErrDemo),
        "\n", err2, errors.Is(err2, ErrDemo),
        "\n", err3, errors.Is(err3, ErrDemo))
}
​
// 123 true
// error: "from main" 123 true
// error: error: from main 123 又一个错误 true

上面的例子中,我们使用 fmt.Errorf 即可以添加额外信息,又可以使用 errors.Is来判断当前 error 的具体类型

同时,我们也可以通过 errors.As去指定去重其中某个类型的错误。

panic

我们不建议在业务代码中去使用 panic ,因为如果调用函数不包括 recover 会造成程序崩溃。如果问题可以被屏蔽或解决,我们可以使用 error 实现。

但是有些最基础的功能,例如连接数据库,实现消息队列等,我们需要尽早的暴露这里面出现的错误(因为如果基础功能出现了错误,上面的业务可能大部分都用不了),此时可以使用 panic

deffer

defer 语句可以尽早的写在函数前。

func Func() {
    defer fmt.Printf("1")
    ...
}

另外,如果一个函数中出现了多个 defer 函数,defer 语句会遵循后进先出:

func Func() {
    defer fmt.Printf("1")
    defer fmt.Printf("2")
}
// 2 1

代码风格

代码格式方面,我们可以使用 gofmt 。它作为官方提供的工具,能自动格式化Go语言代码为官方的统一风格。并且常见的 IDE 都内置或具有相关的插件,可以很方便的配置。

另一方面我们也可以使用 Go 语言官方提供的工具 goimports,实际等于 gofmt 加上依赖包管理,可以自动增删依赖的包引用,将依赖包按字母排序并分类。

Goland 已经内置了改工具,我们可以在 Setting -> Tools -> Actions on Save 中找到 Reformat codeOptimize imports 并勾选上来开启相关功能。在 Setting -> Editor -> Code Style -> Go 找到 Imports 下的 排序风格来选择使用 gofmt 还是 goimports

当然 Goland 一般都已经把这些提前配置了,个人更推荐使用 goimports 管理包引用,而代码风格,引入风格等都使用 gofmt