程序锅

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于

  • 搜索
基础知识 Etcd LeetCode 计算机体系结构 Kubernetes Containerd Docker 容器 云原生 Serverless 项目开发维护 ELF 深入理解程序 Tmux Vim Linux Kernel Linux numpy matplotlib 机器学习 MQTT 网络基础 Thrift RPC OS 操作系统 Clang 研途 数据结构和算法 Java 编程语言 Golang Python 个人网站搭建 Nginx 计算机通用技术 Git

Go | Go 编码规范

发表于 2021-07-17 | 分类于 Golang | 0 | 阅读次数 2699

1. 代码格式

  • 运算符和操作数之间要留空格。
  • 建议一行代码、注释不超过 120 个字符,超过部分,请采用合适的换行方式换行。但也有些例外场景,例如 import 行、工具自动生成的代码、带 tag 的 struct 字段。
  • 文件长度不能超过 800 行。
  • 函数长度不能超过 80 行。
  • 代码都必须用 gofmt 进行格式化。

2. 命名规范---参考「命名规范」

2.1. 项目名

  • 项目名可以通过中划线来连接多个单词。

2.2. 文件名

  • 文件名应小写,可使用下划线分割单词。
  • 文件名要简短有意义。

2.3. 包命名

  • 包名必须和目录名一致,尽量采取有意义、简短的包名,不要和标准库冲突。
  • 包名全部小写,没有大写或下划线,使用多级目录来划分层级。
  • 包名以及包所在的目录名,不要使用复数,例如,是net/url,而不是net/urls。
  • 不要用 common、util、shared 或者 lib 这类宽泛的、无意义的包名。
  • 包名要简单明了,例如 net、time、log。

2.4. 接口命名

接口命名的规则,基本和结构体命名规则保持一致:

  • 单个函数的接口名以 “er"”作为后缀(例如 Reader,Writer),有时候可能导致蹩脚的英文,但是没关系。
  • 两个函数的接口名以两个函数名命名,例如 ReadWriter。
  • 三个以上函数的接口名,类似于结构体名。
// Seeking to an offset before the start of the file is an error.
// Seeking to any positive offset is legal, but the behavior of subsequent
// I/O operations on the underlying object is implementation-dependent.
type Seeker interface {
  Seek(offset int64, whence int) (int64, error)
}

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
  Reader
  Writer
}

2.5. 结构体命名

  • 采用驼峰命名方式,首字母根据访问控制决定使用大写或小写,例如 MixedCaps 或者 mixedCaps。

  • 结构体名不应该是动词,应该是名词,比如 Node、NodeSpec。

  • 避免使用 Data、Info 这类无意义的结构体名。

  • 结构体的声明和初始化应采用多行,例如:

    // User 多行声明
    type User struct {
        Name  string
        Email string
    }
    
    // 多行初始化
    u := User{
        UserName: "colin",
        Email:    "colin404@foxmail.com",
    }
    

2.6. 函数命名

  • 函数名采用驼峰式,首字母根据访问控制决定使用大写或小写,例如:MixedCaps 或者 mixedCaps。

    代码生成工具自动生成的代码 (如 xxxx.pb.go) 和为了对相关测试用例进行分组,而采用的下划线 (如 TestMyFunction_WhatIsBeingTested) 排除此规则。

2.7. 常量命名

  • 常量名必须遵循驼峰式,首字母根据访问控制决定使用大写或小写。

  • 如果是枚举类型的常量,需要先创建相应类型:

    // Code defines an error code type.
    type Code int
    
    // Internal errors.
    const (
        // ErrUnknown - 0: An unknown error occurred.
        ErrUnknown Code = iota
        // ErrFatal - 1: An fatal error occurred.
        ErrFatal
    )
    

2.8. 变量命名

  • 变量名必须遵循驼峰式,首字母根据访问控制决定使用大写或小写。

  • 在相对简单(对象数量少、针对性强)的环境中,可以将一些名称由完整单词简写为单个字母,例如:

    • user 可以简写为 u;
    • userID 可以简写 uid。
  • 特有名词时,需要遵循以下规则:

    • 如果变量为私有,且特有名词为首个单词,则使用小写,如 apiClient。
    • 其他情况都应当使用该名词原有的写法,如 APIClient、repoID、UserID。

    下面列举了一些常见的特有名词:

    // A GonicMapper that contains a list of common initialisms taken from golang/lint
    var LintGonicMapper = GonicMapper{
        "API":   true,
        "ASCII": true,
        "CPU":   true,
        "CSS":   true,
        "DNS":   true,
        "EOF":   true,
        "GUID":  true,
        "HTML":  true,
        "HTTP":  true,
        "HTTPS": true,
        "ID":    true,
        "IP":    true,
        "JSON":  true,
        "LHS":   true,
        "QPS":   true,
        "RAM":   true,
        "RHS":   true,
        "RPC":   true,
        "SLA":   true,
        "SMTP":  true,
        "SSH":   true,
        "TLS":   true,
        "TTL":   true,
        "UI":    true,
        "UID":   true,
        "UUID":  true,
        "URI":   true,
        "URL":   true,
        "UTF8":  true,
        "VM":    true,
        "XML":   true,
        "XSRF":  true,
        "XSS":   true,
    }
    
  • 若变量类型为 bool 类型,则名称应以 Has,Is,Can 或 Allow 开头,例如:

    var hasConflict bool
    var isExist bool
    var canManage bool
    var allowGitHook bool
    
  • 局部变量应当尽可能短小,比如使用 buf 指代 buffer,使用 i 指代 index。

  • 代码生成工具自动生成的代码可排除此规则 (如 xxx.pb.go 里面的 Id)。

  • 对于未导出的顶层常量和变量,使用 _ 作为前缀。

    // bad
    const (
      defaultHost = "127.0.0.1"
      defaultPort = 8080
    )
    
    // good
    const (
      _defaultHost = "127.0.0.1"
      _defaultPort = 8080
    )
    

3. 注释规范---参考「注释规范」

  • 全部使用单行注释,禁止使用多行注释。

  • 和代码的规范一样,单行注释不要过长,禁止超过 120 字符,超过的请使用换行展示,尽量保持格式优雅。

  • 注释必须是完整的句子,以需要注释的内容作为开头,句点作为结尾,格式为 // 名称 描述. 。例如:

    // bad
    // logs the flags in the flagset.
    func PrintFlags(flags *pflag.FlagSet) {
      // normal code
    }
    
    // good
    // PrintFlags logs the flags in the flagset.
    func PrintFlags(flags *pflag.FlagSet) {
      // normal code
    }
    
  • 在多段注释之间可以使用空行分隔加以区分,如下所示:

    // Package superman implements methods for saving the world.
    //
    // Experience has shown that a small number of procedures can prove
    // helpful when attempting to save the world.
    package superman
    
  • 所有注释掉的代码在提交 code review 前都应该被删除,否则应该说明为什么不删除,并给出后续处理建议。

  • 每个可导出的名字都要有注释,该注释对导出的变量、常量、函数、结构体、接口等进行简要介绍。

3.1. 包注释

  • 每个包都有且仅有一个包级别的注释,比如一个包里有多个文件,但是只能有一个文件需要加注释,可以考虑加一个 doc.go,然后这个文件里只放注释,如下所示

    // Package cache is a client-side caching mechanism. It is useful for
    // reducing the number of server calls you'd otherwise need to make.
    // Reflector watches a server and updates a Store. Two stores are provided;
    // one that simply caches objects (for example, to allow a scheduler to
    // list currently available nodes), and one that additionally acts as
    // a FIFO queue (for example, to allow a scheduler to process incoming
    // pods).
    package cache // import "k8s.io/client-go/tools/cache"
    
  • 包注释统一用 // 进行注释,格式为 // Package 包名 包描述 ,例如:

    // Package genericclioptions contains flags which can be added to you command, bound, completed, and produce
    // useful helper functions.
    package genericclioptions
    

3.2. 结构体注释

  • 每个需要导出的结构体或者接口都必须有注释说明,格式为 // 结构体名 结构体描述。

  • 结构体内的可导出成员变量名,如果意义不明确,必须要给出注释,放在成员变量的前一行或同一行的末尾。例如:

    // User represents a user restful resource. It is also used as gorm model.
    type User struct {
        // Standard object's metadata.
        metav1.ObjectMeta `json:"metadata,omitempty"`
    
        Nickname string `json:"nickname" gorm:"column:nickname"`
        Password string `json:"password" gorm:"column:password"`
        Email    string `json:"email" gorm:"column:email"`
        Phone    string `json:"phone" gorm:"column:phone"`
        IsAdmin  int    `json:"isAdmin,omitempty" gorm:"column:isAdmin"`
    }
    

3.3. 类型注释

  • 每个需要导出的类型定义和类型别名都必须有注释说明,格式为 // 类型名 类型描述. ,例如:

    // Code defines an error code type.
    type Code int
    

3.4. 函数注释

  • 每个需要导出的函数或者方法都必须有注释,格式为// 函数名 函数描述。例如:

    // BeforeUpdate run before update database record.
    func (p *Policy) BeforeUpdate() (err error) {
      // normal code
      return nil
    }
    

3.5. 变量/常量注释

  • 每个可导出的变量 / 常量都必须有注释说明,格式为// 变量名 变量描述,例如:

    // ErrSigningMethod defines invalid signing method error.
    var ErrSigningMethod = errors.New("Invalid signing method")
    
  • 出现大块常量或变量定义时,可在前面注释一个总的说明,然后在每一行常量的前一行或末尾详细注释该常量的定义,例如:

    // Code must start with 1xxxxx.    
    const (                         
        // ErrSuccess - 200: OK.          
        ErrSuccess int = iota + 100001    
    
        // ErrUnknown - 500: Internal server error.    
        ErrUnknown    
    
        // ErrBind - 400: Error occurred while binding the request body to the struct.    
        ErrBind    
    
        // ErrValidation - 400: Validation failed.    
        ErrValidation 
    )
    

4. 声明/初始化/定义

  • 变量声明尽量放在变量第一次使用的前面,遵循就近原则。

  • 当函数中需要使用到多个变量时,可以在函数开始处使用 var 声明。在函数外部声明必须使用 var ,不要采用 := ,容易踩到变量的作用域的问题。

    var (
      Width  int
      Height int
    )
    
  • 在顶层,使用标准 var 关键字。请勿指定类型,除非它与表达式的类型不同。

    // bad
    var _s string = F()
    
    func F() string { return "A" }
    
    // good
    var _s = F()
    // 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型
    // 还是那种类型
    
    func F() string { return "A" }
    
  • 相似的声明放在一组,同样适用于常量、变量和类型声明。

    // bad
    import "a"
    import "b"
    
    // good
    import (
      "a"
      "b"
    )
    
  • 在初始化结构引用时,请使用 &T{}代替 new(T),以使其与结构体初始化一致。

    // bad
    sptr := new(T)
    sptr.Name = "bar"
    
    // good
    sptr := &T{Name: "bar"}
    
  • struct 声明和初始化格式采用多行,定义如下。

    type User struct{
        Username  string
        Email     string
    }
    
    user := User{
      Username: "colin",
      Email: "colin404@foxmail.com",
    }
    
  • 嵌入式类型(例如 mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。

    // bad
    type Client struct {
      version int
      http.Client
    }
    
    // good
    type Client struct {
      http.Client
    
      version int
    }
    
  • 尽可能指定容器容量,以便为容器预先分配内存.

    v := make(map[int]string, 4)
    v := make([]string, 0, 4)
    

    假如不指定的话,请使用如下的方式

    // bad
    s := []string{}
    s := make([]string, 0)
    
    // good
    var s []string
    
  • 如果魔法数字出现超过两次,则禁止使用,改用一个常量代替,例如:

    // PI ...
    const Prise = 3.14
    
    func getAppleCost(n float64) float64 {
      return Prise * n
    }
    
    func getOrangeCost(n float64) float64 {
      return Prise * n
    }
    

5. 函数/方法

5.1. 函数参数

  • 传入变量和返回变量以小写字母开头。

  • 函数参数个数不能超过 5 个。

  • 尽量采用值传递,而非指针传递。如果结构体很大,也可以传指针。

  • 传入参数是 map、slice、chan、interface ,不要传递指针。

  • 如果函数返回相同类型的两个或三个参数,或者如果从上下文中不清楚结果的含义,使用命名返回,其他情况不建议使用命名返回,例如:

    func coordinate() (x, y float64, err error) {
      // normal code
    }
    
  • 多返回值最多返回三个,超过三个请使用 struct。

5.2. 方法的接收器

  • 推荐以类名第一个英文首字母的小写作为接收器的命名。
  • 接收器的命名在函数超过 20 行的时候不要用单字符。
  • 接收器的命名不能采用 me、this、self 这类易混淆名称。

5.3. 其他

  • 嵌套深度不能超过 4 层。
  • 函数分组与顺序
    • 函数应按粗略的调用顺序排序。
    • 同一文件中的函数应按接收者分组。

6. import

  • 代码都必须用 goimports 进行格式化(建议将代码 Go 代码编辑器设置为:保存时运行 goimports)。

  • 不要使用相对路径引入包,例如 import ../util/net 。

  • 包名称与导入路径的最后一个目录名不匹配时,或者多个相同包名冲突时,则必须使用导入别名。

    // bad
    "github.com/dgrijalva/jwt-go/v4"
    
    //good
    jwt "github.com/dgrijalva/jwt-go/v4"
    
  • 导入的包建议进行分组,匿名包的引用使用一个新的分组,并对匿名包引用进行说明。

    import (
      // go 标准包
      "fmt"
    
      // 第三方包
      "github.com/jinzhu/gorm"
      "github.com/spf13/cobra"
      "github.com/spf13/viper"
    
      // 匿名包单独分组,并对匿名包引用进行说明
      // import mysql driver
      _ "github.com/jinzhu/gorm/dialects/mysql"
    
      // 内部包
      v1 "github.com/marmotedu/api/apiserver/v1"
      metav1 "github.com/marmotedu/apimachinery/pkg/meta/v1"
      "github.com/marmotedu/iam/pkg/cli/genericclioptions"
    )
    

7. defer

  • 当存在资源创建时,应紧跟 defer 释放资源 (可以大胆使用 defer,defer 在 Go1.14 版本中,性能大幅提升,defer 的性能损耗即使在性能敏感型的业务中,也可以忽略)。

  • 先判断是否错误,再 defer 释放资源,例如:

    rep, err := http.Get(url)
    if err != nil {
        return err
    }
    
    defer resp.Body.Close()
    

8. panic

  • 在业务逻辑处理中禁止使用 panic。
  • 在 main 包中,只有当程序完全不可运行时使用 panic,例如无法打开文件、无法连接数据库导致程序无法正常运行。
  • 在 main 包中,使用 log.Fatal 来记录错误,这样就可以由 log 来结束程序,或者将 panic 抛出的异常记录到日志文件中,方便排查问题。
  • 可导出的接口一定不能有 panic。
  • 包内建议采用 error 而不是 panic 来传递错误。

9. Error

9.1. Error 的命名

  • Error 类型应该写成 xxxError 的形式
  • Error 变量应该写成 Errxxx 的形式
type ExitError struct {
  // ....
}

var ErrFormat = errors.New("unknown format")

9.2. Error 处理

  • error作为函数的值返回,必须对error进行处理,或将返回值赋值给明确忽略。对于defer xx.Close()可以不用显式处理。

    func load() error {
      // normal code
    }
    
    // bad
    load()
    
    // good
     _ = load()
    
  • error作为函数的值返回且有多个返回值的时候,error必须是最后一个参数。

    // bad
    func load() (error, int) {
      // normal code
    }
    
    // good
    func load() (int, error) {
      // normal code
    }
    
  • 尽早进行错误处理,并尽早返回,减少嵌套。

    // bad
    if err != nil {
      // error code
    } else {
      // normal code
    }
    
    // good
    if err != nil {
      // error handling
      return err
    }
    // normal code
    
  • 错误要单独判断,不与其他逻辑组合判断。

    // bad
    v, err := foo()
    if err != nil || v  == nil {
      // error handling
      return err
    }
    
    // good
    v, err := foo()
    if err != nil {
      // error handling
      return err
    }
    
    if v == nil {
      // error handling
      return errors.New("invalid value v")
    }
    
  • 如果需要在 if 之外使用函数调用的结果,则应采用下面的方式。

    // bad
    if v, err := foo(); err != nil {
      // error handling
    }
    
    // good
    v, err := foo()
    if err != nil {
      // error handling
    }
    
  • 如果返回值需要初始化,则采用下面的方式。

    v, err := f()
    if err != nil {
        // error handling
        return // or continue.
    }
    // use v
    

9.3. Error 错误描述建议

  • 告诉用户他们可以做什么,而不是告诉他们不能做什么。
  • 当声明一个需求时,用 must 而不是 should。例如,must be greater than 0、must match regex '[a-z]+'。
  • 当声明一个格式不对时,用 must not。例如,must not contain。
  • 当声明一个动作时用 may not。例如,may not be specified when otherField is empty、only name may be specified。
  • 引用文字字符串值时,请在单引号中指示文字。例如,ust not contain '..'。
  • 当引用另一个字段名称时,请在反引号中指定该名称。例如,must be greater than request。
  • 指定不等时,请使用单词而不是符号。例如,must be less than 256、must be greater than or equal to 0 (不要用 larger than、bigger than、more than、higher than)。
  • 指定数字范围时,请尽可能使用包含范围。
  • 建议 Go 1.13 以上,error 生成方式为 fmt.Errorf("module xxx: %w", err)。
  • 错误描述用小写字母开头,结尾不要加标点符号,例如:
// bad
errors.New("Redis connection failed")
errors.New("redis connection failed.")

// good
errors.New("redis connection failed")

10. 类型处理

10.1. 字符串

  • string 表示的是不可变的字符串变量,对 string 的修改是比较重的操作,基本上都需要重新申请内存。所以,如果没有特殊需要,需要修改时多使用 []byte。

  • 优先使用 strconv 而不是 fmt。

  • 空字符串判断,使用 len() 函数。

    https://stackoverflow.com/questions/18594330/what-is-the-best-way-to-test-for-an-empty-string-in-go 这里有一份说明,编译器针对这两种情况,编译出来的代码是相同的,所以这两种怎么用都行。

    // bad
    if s == "" {
        // normal code
    }
    
    // good
    if len(s) == 0 {
        // normal code
    }
    
  • []byte/string 相等比较,使用 compare 函数。

    // bad
    var s1 []byte
    var s2 []byte
    ...
    bytes.Equal(s1, s2) == 0
    bytes.Equal(s1, s2) != 0
    
    // good
    var s1 []byte
    var s2 []byte
    ...
    bytes.Compare(s1, s2) == 0
    bytes.Compare(s1, s2) != 0
    
  • 字符串是否包含子串或字符,使用 Index 函数。

    // bad
    strings.Contains(s, subStr)
    strings.ContainsAny(s, char)
    strings.ContainRune(s, r)
    
    // good
    strings.Index(s, subStr) > -1
    strings.IndexAny(s, char) > -1
    strings.IndexRune(s, r) > -1
    
  • 去除前后子串。

    // bad
    var s1 = "a string value"
    var s2 = "a "
    var s3 = strings.TrimPrefix(s1, s2)
    
    // good
    var s1 = "a string value"
    var s2 = "a "
    var s3 string
    if strings.HasPrefix(s1, s2) {
        s3 = s1[len(s2):]
    }
    
  • 复杂字符串使用 raw 字符串避免字符转义。

    // bad
    regexp.MustCompile("\\.")
    
    // good
    regexp.MustCompile(`\.`)
    

10.2. 切片

  • 空 slice 判断,这种判断同样适用于 map、channel。

    // bad
    if len(slice) = 0 {
        // normal code
    }
    
    // good
    if slice != nil && len(slice) == 0 {
        // normal code
    }
    
  • slice 复制。

    // bad
    var b1, b2 []byte
    for i, v := range b1 {
       b2[i] = v
    }
    for i := range b1 {
       b2[i] = b1[i]
    }
    
    // good
    copy(b2, b1)
    
  • slice 新增。需要注意的是:append 要小心自动分配内存,append 返回的可能是新分配的地址。

    // bad
    var a, b []int
    for _, v := range a {
        b = append(b, v)
    }
    
    // good
    var a, b []int
    b = append(b, a...)
    

10.3. map

  • 如果要直接修改 map 的 value 值,则 value 只能是指针,否则要覆盖原来的值。
  • map 在并发中需要加锁。

10.4. 结构体

  • struct 以多行格式初始化。

    type user struct {
      Id   int64
      Name string
    }
    
    u1 := user{100, "Colin"}
    
    u2 := user{
        Id:   200,
        Name: "Lex",
    }
    

10.5. 类型断言/转换

  • 类型断言失败处理

    type assertion 的单个返回值针对不正确的类型将产生 panic。请始终使用 “comma ok”的惯用法。

    // bad
    t := n.(int)
    
    // good
    t, ok := n.(int)
    if !ok {
      // error handling
    }
    // normal code
    
  • 类型转换

    编译过程无法检查 interface{} 的转换,只能在运行时检查,小心引起 panic。

11. 控制结构

11.1. if

  • if 可以接受初始化语句,约定如下方式建立局部变量。

    if err := loadConfig(); err != nil {
      // error handling
      return err
    }
    
  • if 对于 bool 类型的变量,应直接进行真假判断。

    var isAllow bool
    if isAllow {
      // normal code
    }
    

11.2. for

  • 采用短声明建立局部变量

    sum := 0
    for i := 0; i < 10; i++ {
        sum += 1
    }
    
  • 不要在 for 循环里面使用 defer,defer 只有在函数退出时才会执行。

    // bad
    for file := range files {
      fd, err := os.Open(file)
      if err != nil {
        return err
      }
      defer fd.Close()
      // normal code
    }
    
    // good
    for file := range files {
      func() {
        fd, err := os.Open(file)
        if err != nil {
          return err
        }
        defer fd.Close()
        // normal code
      }()
    }
    

11.3. range

  • 如果只需要第一项(key),就丢弃第二个。

    for key := range keys {
    // normal code
    }
    
  • 如果只需要第二项,则把第一项置为下划线。

    sum := 0
    for _, value := range array {
        sum += value
    }
    

11.4. switch

  • 必须要有 default

    switch os := runtime.GOOS; os {
        case "linux":
            fmt.Println("Linux.")
        case "darwin":
            fmt.Println("OS X.")
        default:
            fmt.Printf("%s.\n", os)
    }
    

11.5. goto

  • 业务代码禁止使用 goto 。
  • 框架或其他底层源码尽量不用。

12. 其他

  • 尽量少用全局变量,而是通过参数传递,使每个函数都是“无状态”的。这样可以减少耦合,也方便分工和单元测试。

  • 服务器处理请求时,应该创建一个 context,保存该请求的相关信息(如 requestID),并在函数调用链中传递。

  • 在编译时验证接口的符合性,例如:

    type LogHandler struct {
      h   http.Handler
      log *zap.Logger
    }
    var _ http.Handler = LogHandler{}
    

13. 单元测试

  • 单元测试文件名命名规范为 example_test.go。
  • 每个重要的可导出函数都要编写测试用例。
  • 由于单元测试文件内的函数都是不对外的,所以可导出的结构体、函数等可以不带注释。
  • 如果存在 func (b *Bar) Foo ,单测函数可以为 func TestBar_Foo。

14. 项目管理

14.1. GOPATH 设置

  • Go 1.11 之后,弱化了 GOPATH 规则,已有代码(很多库肯定是在 1.11 之前建立的)肯定符合这个规则,建议保留 GOPATH 规则,便于维护代码。
  • 建议只使用一个 GOPATH,不建议使用多个 GOPATH。如果使用多个 GOPATH,编译生效的 bin 目录是在第一个 GOPATH 下。

14.2. 依赖管理

  • Go 1.11 以上必须使用 Go Modules。
  • 使用 Go Modules 作为依赖管理的项目时,不建议提交 vendor 目录,但必须提交 go.sum 文件。
卷死我
dawnguo 微信支付

微信支付

dawnguo 支付宝

支付宝

  • 本文作者: dawnguo
  • 本文链接: /archives/18
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# Golang # 编程语言
项目开发维护 | 版本规范
数据结构和算法 | 无锁链表/队列的实现【纯纯的代码】
  • 文章目录
  • 站点概览
dawnguo

dawnguo

215 日志
24 分类
37 标签
RSS
Creative Commons
© 2018 — 2025 程序锅
0%