在 Go 项目开发中,一个好的编码规范可以极大地提高代码质量。为了帮你节省时间和精力,这里我整理了一份清晰、可直接套用的 Go 编码规范,供你参考。

这份规范,是我参考了 Go 官方提供的编码规范,以及 Go 社区沉淀的一些比较合理的规范之后,加入自己的理解总结出的,它比很多公司内部的规范更全面,你掌握了,以后在面试大厂的时候,或者在大厂里写代码的时候,都会让人高看一眼,觉得你 code 很专业。

这份编码规范中包含代码风格、命名规范、注释规范、类型、控制结构、函数、GOPATH 设置规范、依赖管理和最佳实践九类规范。如果你觉得这些规范内容太多了,看完一遍也记不住,这完全没关系。你可以多看几遍,也可以在用到时把它翻出来,在实际应用中掌握。这篇特别放送的内容,更多是作为写代码时候的一个参考手册。

1. 代码风格

1.1 代码格式

  • 代码都必须用 gofmt 进行格式化。

  • 运算符和操作数之间要留空格。

  • 建议一行代码不超过 120 个字符,超过部分,请采用合适的换行方式换行。但也有些例外场景,例如 import 行、工具自动生成的代码、带 tag 的 struct 字段。

  • 文件长度不能超过 800 行。

  • 函数长度不能超过 80 行。

  • import 规范

    • 代码都必须用goimports进行格式化(建议将 Go 代码编辑器设置为:保存时运行 goimports)。
    • 不要使用相对路径引入包,例如 import ../util/net
    • 包名称与导入路径的最后一个目录名不匹配时,或者多个相同包名冲突时,则必须使用导入别名。
  1. // bad
  2. "github.com/dgrijalva/jwt-go/v4"
  3. //good
  4. jwt "github.com/dgrijalva/jwt-go/v4"

- 导入的包建议进行分组,匿名包的引用使用一个新的分组,并对匿名包引用进行说明。

  1. import (
  2. // go 标准包
  3. "fmt"
  4. // 第三方包
  5. "github.com/jinzhu/gorm"
  6. "github.com/spf13/cobra"
  7. "github.com/spf13/viper"
  8. // 匿名包单独分组,并对匿名包引用进行说明
  9. // import mysql driver
  10. _ "github.com/jinzhu/gorm/dialects/mysql"
  11. // 内部包
  12. v1 "github.com/marmotedu/api/apiserver/v1"
  13. metav1 "github.com/marmotedu/apimachinery/pkg/meta/v1"
  14. "github.com/marmotedu/iam/pkg/cli/genericclioptions"
  15. )

1.2 声明、初始化和定义

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

  1. var (
  2. Width int
  3. Height int
  4. )
  • 在初始化结构引用时,请使用&T{}代替new(T),以使其与结构体初始化一致。
  1. // bad
  2. sptr := new(T)
  3. sptr.Name = "bar"
  4. // good
  5. sptr := &T{Name: "bar"}
  • struct 声明和初始化格式采用多行,定义如下。
  1. type User struct{
  2. Username string
  3. Email string
  4. }
  5. user := User{
  6. Username: "colin",
  7. Email: "colin404@foxmail.com",
  8. }
  • 相似的声明放在一组,同样适用于常量、变量和类型声明。
  1. // bad
  2. import "a"
  3. import "b"
  4. // good
  5. import (
  6. "a"
  7. "b"
  8. )
  • 尽可能指定容器容量,以便为容器预先分配内存,例如:
  1. v := make(map[int]string, 4)
  2. v := make([]string, 0, 4)
  • 在顶层,使用标准 var 关键字。请勿指定类型,除非它与表达式的类型不同。
  1. // bad
  2. var _s string = F()
  3. func F() string { return "A" }
  4. // good
  5. var _s = F()
  6. // 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型
  7. // 还是那种类型
  8. func F() string { return "A" }
  • 对于未导出的顶层常量和变量,使用_作为前缀。
  1. // bad
  2. const (
  3. defaultHost = "127.0.0.1"
  4. defaultPort = 8080
  5. )
  6. // good
  7. const (
  8. _defaultHost = "127.0.0.1"
  9. _defaultPort = 8080
  10. )
  • 嵌入式类型(例如 mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。
  1. // bad
  2. type Client struct {
  3. version int
  4. http.Client
  5. }
  6. // good
  7. type Client struct {
  8. http.Client
  9. version int
  10. }

1.3 错误处理

  • error作为函数的值返回,必须对error进行处理,或明确忽略 error 返回值。对于defer xx.Close(),可以不用显式处理。
  1. func load() error {
  2. // normal code
  3. }
  4. // bad
  5. load()
  6. // good
  7. _ = load()
  • error作为函数的值返回且有多个返回值的时候,error必须是最后一个参数。
  1. // bad
  2. func load() (error, int) {
  3. // normal code
  4. }
  5. // good
  6. func load() (int, error) {
  7. // normal code
  8. }
  • 尽早进行错误处理,并尽早返回,减少嵌套。
  1. // bad
  2. if err != nil {
  3. // error code
  4. } else {
  5. // normal code
  6. }
  7. // good
  8. if err != nil {
  9. // error handling
  10. return err
  11. }
  12. // normal code
  • 如果需要在 if 之外使用函数调用的结果,则应采用下面的方式。
  1. // bad
  2. if v, err := foo(); err != nil {
  3. // error handling
  4. }
  5. // good
  6. v, err := foo()
  7. if err != nil {
  8. // error handling
  9. }
  • 错误要单独判断,不与其他逻辑组合判断。
  1. // bad
  2. v, err := foo()
  3. if err != nil || v == nil {
  4. // error handling
  5. return err
  6. }
  7. // good
  8. v, err := foo()
  9. if err != nil {
  10. // error handling
  11. return err
  12. }
  13. if v == nil {
  14. // error handling
  15. return errors.New("invalid value v")
  16. }
  • 如果返回值需要初始化,则采用下面的方式。
  1. v, err := f()
  2. if err != nil {
  3. // error handling
  4. return // or continue.
  5. }
  • 错误描述建议

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

1.4 panic 处理

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

1.5 单元测试

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

1.6 类型断言失败处理

  • type assertion 的单个返回值针对不正确的类型将产生 panic。请始终使用 “comma ok”的惯用法。
  1. // bad
  2. t := n.(int)
  3. // good
  4. t, ok := n.(int)
  5. if !ok {
  6. // error handling
  7. }

2. 命名规范

命名规范是代码规范中非常重要的一部分,一个统一的、短小的、精确的命名规范可以大大提高代码的可读性,也可以借此规避一些不必要的 Bug。

2.1 包命名

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

2.2 函数命名

  • 函数名采用驼峰式,首字母根据访问控制决定使用大写或小写,例如:MixedCaps或者mixedCaps
  • 代码生成工具自动生成的代码(如xxxx.pb.go)和为了对相关测试用例进行分组,而采用的下划线(如TestMyFunction_WhatIsBeingTested)排除此规则。

2.3 文件命名

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

2.4 结构体命名

  • 采用驼峰命名方式,首字母根据访问控制决定使用大写或小写,例如MixedCaps或者mixedCaps
  • 结构体名不应该是动词,应该是名词,比如 NodeNodeSpec
  • 避免使用 Data、Info 这类无意义的结构体名。
  • 结构体的声明和初始化应采用多行,例如:
  1. // User 多行声明
  2. type User struct {
  3. Name string
  4. Email string
  5. }
  6. // 多行初始化
  7. u := User{
  8. UserName: "colin",
  9. Email: "colin404@foxmail.com",
  10. }

2.5 接口命名

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

    • 单个函数的接口名以 er 作为后缀(例如Reader,Writer),有时候可能导致蹩脚的英文,但是没关系。
    • 两个函数的接口名以两个函数名命名,例如 ReadWriter。
    • 三个以上函数的接口名,类似于结构体名。

例如:

  1. // Seeking to an offset before the start of the file is an error.
  2. // Seeking to any positive offset is legal, but the behavior of subsequent
  3. // I/O operations on the underlying object is implementation-dependent.
  4. type Seeker interface {
  5. Seek(offset int64, whence int) (int64, error)
  6. }
  7. // ReadWriter is the interface that groups the basic Read and Write methods.
  8. type ReadWriter interface {
  9. Reader
  10. Writer
  11. }

2.6 变量命名

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

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

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

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

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

  1. // A GonicMapper that contains a list of common initialisms taken from golang/lint
  2. var LintGonicMapper = GonicMapper{
  3. "API": true,
  4. "ASCII": true,
  5. "CPU": true,
  6. "CSS": true,
  7. "DNS": true,
  8. "EOF": true,
  9. "GUID": true,
  10. "HTML": true,
  11. "HTTP": true,
  12. "HTTPS": true,
  13. "ID": true,
  14. "IP": true,
  15. "JSON": true,
  16. "LHS": true,
  17. "QPS": true,
  18. "RAM": true,
  19. "RHS": true,
  20. "RPC": true,
  21. "SLA": true,
  22. "SMTP": true,
  23. "SSH": true,
  24. "TLS": true,
  25. "TTL": true,
  26. "UI": true,
  27. "UID": true,
  28. "UUID": true,
  29. "URI": true,
  30. "URL": true,
  31. "UTF8": true,
  32. "VM": true,
  33. "XML": true,
  34. "XSRF": true,
  35. "XSS": true,
  36. }
  • 若变量类型为 bool 类型,则名称应以 Has,Is,Can 或 Allow 开头,例如:
  1. var hasConflict bool
  2. var isExist bool
  3. var canManage bool
  4. var allowGitHook bool
  • 局部变量应当尽可能短小,比如使用 buf 指代 buffer,使用 i 指代 index。
  • 代码生成工具自动生成的代码可排除此规则(如xxx.pb.go里面的 Id)

2.7 常量命名

  • 常量名必须遵循驼峰式,首字母根据访问控制决定使用大写或小写。
  • 如果是枚举类型的常量,需要先创建相应类型:
  1. // Code defines an error code type.
  2. type Code int
  3. // Internal errors.
  4. const (
  5. // ErrUnknown - 0: An unknown error occurred.
  6. ErrUnknown Code = iota
  7. // ErrFatal - 1: An fatal error occurred.
  8. ErrFatal
  9. )

2.8 Error 的命名

  • Error 类型应该写成 FooError 的形式。
  1. type ExitError struct {
  2. // ....
  3. }
  • Error 变量写成 ErrFoo 的形式。
  1. var ErrFormat = errors.New("unknown format")

3. 注释规范

  • 每个可导出的名字都要有注释,该注释对导出的变量、函数、结构体、接口等进行简要介绍。
  • 全部使用单行注释,禁止使用多行注释。
  • 和代码的规范一样,单行注释不要过长,禁止超过 120 字符,超过的请使用换行展示,尽量保持格式优雅。
  • 注释必须是完整的句子,以需要注释的内容作为开头,句点作为结尾,格式为 // 名称 描述.。例如:
  1. // bad
  2. // logs the flags in the flagset.
  3. func PrintFlags(flags *pflag.FlagSet) {
  4. // normal code
  5. }
  6. // good
  7. // PrintFlags logs the flags in the flagset.
  8. func PrintFlags(flags *pflag.FlagSet) {
  9. // normal code
  10. }
  • 所有注释掉的代码在提交 code review 前都应该被删除,否则应该说明为什么不删除,并给出后续处理建议。
  • 在多段注释之间可以使用空行分隔加以区分,如下所示:
  1. // Package superman implements methods for saving the world.
  2. //
  3. // Experience has shown that a small number of procedures can prove
  4. // helpful when attempting to save the world.
  5. package superman

3.1 包注释

  • 每个包都有且仅有一个包级别的注释。
  • 包注释统一用 // 进行注释,格式为 // Package 包名 包描述,例如:
  1. // Package genericclioptions contains flags which can be added to you command, bound, completed, and produce
  2. // useful helper functions.
  3. package genericclioptions

3.2 变量/常量注释

  • 每个可导出的变量/常量都必须有注释说明,格式为// 变量名 变量描述,例如:
  1. // ErrSigningMethod defines invalid signing method error.
  2. var ErrSigningMethod = errors.New("Invalid signing method")
  • 出现大块常量或变量定义时,可在前面注释一个总的说明,然后在每一行常量的前一行或末尾详细注释该常量的定义,例如:
  1. // Code must start with 1xxxxx.
  2. const (
  3. // ErrSuccess - 200: OK.
  4. ErrSuccess int = iota + 100001
  5. // ErrUnknown - 500: Internal server error.
  6. ErrUnknown
  7. // ErrBind - 400: Error occurred while binding the request body to the struct.
  8. ErrBind
  9. // ErrValidation - 400: Validation failed.
  10. ErrValidation
  11. )

3.3 结构体注释

  • 每个需要导出的结构体或者接口都必须有注释说明,格式为 // 结构体名 结构体描述.
  • 结构体内的可导出成员变量名,如果意义不明确,必须要给出注释,放在成员变量的前一行或同一行的末尾。例如:
  1. // User represents a user restful resource. It is also used as gorm model.
  2. type User struct {
  3. // Standard object's metadata.
  4. metav1.ObjectMeta `json:"metadata,omitempty"`
  5. Nickname string `json:"nickname" gorm:"column:nickname"`
  6. Password string `json:"password" gorm:"column:password"`
  7. Email string `json:"email" gorm:"column:email"`
  8. Phone string `json:"phone" gorm:"column:phone"`
  9. IsAdmin int `json:"isAdmin,omitempty" gorm:"column:isAdmin"`
  10. }

3.4 方法注释

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

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

3.5 类型注释

  • 每个需要导出的类型定义和类型别名都必须有注释说明,格式为 // 类型名 类型描述.,例如:
  1. // Code defines an error code type.
  2. type Code int

4. 类型

4.1 字符串

  • 空字符串判断。
  1. // bad
  2. if s == "" {
  3. // normal code
  4. }
  5. // good
  6. if len(s) == 0 {
  7. // normal code
  8. }
  • []byte/string相等比较。
  1. // bad
  2. var s1 []byte
  3. var s2 []byte
  4. ...
  5. bytes.Equal(s1, s2) == 0
  6. bytes.Equal(s1, s2) != 0
  7. // good
  8. var s1 []byte
  9. var s2 []byte
  10. ...
  11. bytes.Compare(s1, s2) == 0
  12. bytes.Compare(s1, s2) != 0
  • 复杂字符串使用 raw 字符串避免字符转义。
  1. // bad
  2. regexp.MustCompile("\.")
  3. // good
  4. regexp.MustCompile(`.`)

4.2 切片

  • 空 slice 判断。
  1. // bad
  2. if len(slice) = 0 {
  3. // normal code
  4. }
  5. // good
  6. if slice != nil && len(slice) == 0 {
  7. // normal code
  8. }

上面判断同样适用于 map、channel。

  • 声明 slice。
  1. // bad
  2. s := []string{}
  3. s := make([]string, 0)
  4. // good
  5. var s []string
  • slice 复制。
  1. // bad
  2. var b1, b2 []byte
  3. for i, v := range b1 {
  4. b2[i] = v
  5. }
  6. for i := range b1 {
  7. b2[i] = b1[i]
  8. }
  9. // good
  10. copy(b2, b1)
  • slice 新增。
  1. // bad
  2. var a, b []int
  3. for _, v := range a {
  4. b = append(b, v)
  5. }
  6. // good
  7. var a, b []int
  8. b = append(b, a...)

4.3 结构体

  • struct 初始化。

Struct 以多行格式初始化。

  1. type user struct {
  2. Id int64
  3. Name string
  4. }
  5. u1 := user{100, "Colin"}
  6. u2 := user{
  7. Id: 200,
  8. Name: "Lex",
  9. }

5. 控制结构

5.1 if

  • if 接受初始化语句,约定如下方式建立局部变量。
  1. if err := loadConfig(); err != nil {
  2. // error handling
  3. return err
  4. }
  • if 对于 bool 类型的变量,应直接进行真假判断。
  1. var isAllow bool
  2. if isAllow {
  3. // normal code
  4. }

5.2 for

  • 采用短声明建立局部变量。
  1. sum := 0
  2. for i := 0; i < 10; i++ {
  3. sum += 1
  4. }
  • 不要在 for 循环里面使用 defer,defer 只有在函数退出时才会执行。
  1. // bad
  2. for file := range files {
  3. fd, err := os.Open(file)
  4. if err != nil {
  5. return err
  6. }
  7. defer fd.Close()
  8. // normal code
  9. }
  10. // good
  11. for file := range files {
  12. func() {
  13. fd, err := os.Open(file)
  14. if err != nil {
  15. return err
  16. }
  17. defer fd.Close()
  18. // normal code
  19. }()
  20. }

5.3 range

  • 如果只需要第一项(key),就丢弃第二个。
  1. for key := range keys {
  2. // normal code
  3. }
  • 如果只需要第二项,则把第一项置为下划线。
  1. sum := 0
  2. for _, value := range array {
  3. sum += value
  4. }

5.4 switch

  • 必须要有 default。
  1. switch os := runtime.GOOS; os {
  2. case "linux":
  3. fmt.Println("Linux.")
  4. case "darwin":
  5. fmt.Println("OS X.")
  6. default:
  7. fmt.Printf("%s.\n", os)
  8. }

5.5 goto

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

6. 函数

  • 传入变量和返回变量以小写字母开头。
  • 函数参数个数不能超过 5 个。
  • 函数分组与顺序。
  • 函数应按粗略的调用顺序排序。
  • 同一文件中的函数应按接收者分组。
  • 尽量采用值传递,而非指针传递。
  • 传入参数是 map、slice、chan、interface ,不要传递指针。

6.1 函数参数

  • 如果函数返回相同类型的两个或三个参数,或者如果从上下文中不清楚结果的含义,使用命名返回,其他情况不建议使用命名返回,例如:
  1. func coordinate() (x, y float64, err error) {
  2. // normal code
  3. }
  • 传入变量和返回变量都以小写字母开头。
  • 尽量用值传递,非指针传递。
  • 参数数量均不能超过 5 个。
  • 多返回值最多返回三个,超过三个请使用 struct。

6.2 defer

  • 当存在资源创建时,应紧跟 defer 释放资源(可以大胆使用 defer,defer 在 Go1.14 版本中,性能大幅提升,defer 的性能损耗即使在性能敏感型的业务中,也可以忽略)。
  • 先判断是否错误,再 defer 释放资源,例如:
  1. rep, err := http.Get(url)
  2. if err != nil {
  3. return err
  4. }
  5. defer resp.Body.Close()

6.3 方法的接收器

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

6.4 嵌套

  • 嵌套深度不能超过 4 层。

6.5 变量命名

  • 变量声明尽量放在变量第一次使用的前面,遵循就近原则。
  • 如果魔法数字出现超过两次,则禁止使用,改用一个常量代替,例如:
  1. // PI ...
  2. const Prise = 3.14
  3. func getAppleCost(n float64) float64 {
  4. return Prise * n
  5. }
  6. func getOrangeCost(n float64) float64 {
  7. return Prise * n
  8. }

7. GOPATH 设置规范

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

8. 依赖管理

  • Go 1.11 以上必须使用 Go Modules。
  • 使用 Go Modules 作为依赖管理的项目时,不建议提交 vendor 目录。
  • 使用 Go Modules 作为依赖管理的项目时,必须提交 go.sum 文件。

9. 最佳实践

  • 尽量少用全局变量,而是通过参数传递,使每个函数都是“无状态”的。这样可以减少耦合,也方便分工和单元测试。
  • 在编译时验证接口的符合性,例如:
  1. type LogHandler struct {
  2. h http.Handler
  3. log *zap.Logger
  4. }
  5. var _ http.Handler = LogHandler{}
  • 服务器处理请求时,应该创建一个 context,保存该请求的相关信息(如 requestID),并在函数调用链中传递。

9.1 性能

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

9.2 注意事项

  • append 要小心自动分配内存,append 返回的可能是新分配的地址。
  • 如果要直接修改 map 的 value 值,则 value 只能是指针,否则要覆盖原来的值。
  • map 在并发中需要加锁。
  • 编译过程无法检查 interface{} 的转换,只能在运行时检查,小心引起 panic。

总结

这里向你介绍了九类常用的编码规范。但最后我要提醒你一句:规范是人定的,你也可以根据需要制定符合你项目的规范,但同时我也建议你采纳这些业界沉淀下来的规范,并通过工具来确保规范的执行。