最全Go编码规范

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

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

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

提示:比腾讯、字节官方 Go 编码规范,还全的 Go 编码规范,也是全网最全。

1. 代码风格

1.1 代码格式

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

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

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

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

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

  • 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"
    )

1.2 声明、初始化和定义

  • 当函数中需要使用到多个变量时,可以在函数开始处使用 var 声明。在函数外部声明必须使用 var ,不要采用 := ,容易踩到变量的作用域的问题。
var (
    Width  int
    Height int
)
  • 在初始化结构引用时,请使用 &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",
}
  • 相似的声明放在一组,同样适用于常量、变量和类型声明。
// bad
import "a"
import "b"

// good
import (
  "a"
  "b"
)
  • 尽可能指定容器容量,以便为容器预先分配内存,例如:
v := make(map[int]string, 4)
v := make([]string, 0, 4)
  • 在顶层,使用标准 var 关键字。请勿指定类型,除非它与表达式的类型不同。
// bad
var _s string = F()

func F() string { return "A" }

// good
var _s = F()
// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型
// 还是那种类型

func F() string { return "A" }
  • 对于未导出的顶层常量和变量,使用 _ 作为前缀。
// bad
const (
  defaultHost = "127.0.0.1"
  defaultPort = 8080
)

// good
const (
  _defaultHost = "127.0.0.1"
  _defaultPort = 8080
)
  • 嵌入式类型(例如 mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。
// bad
type Client struct {
  version int
  http.Client
}

// good
type Client struct {
  http.Client

  version int
}
  • 对于 struct 的变量初始化,必须加上 struct 的 field。
// 反例
params := request.Params{
    "http://example.com",
    "POST",
    map[string]string{"Authentication": "someToken"}
    someStruct,
}
// 正例
params := request.Params{
    URL: "http://example.com",
    Method: "POST",
    Headers: map[string]string{"Authentication": "someToken"}
    Body: someStruct,
}
  • 如果 struct 中的域需要进行 json 序列化,则需要以大写字母开头,同时要进行 json 命名

说明:结构体中的变量以大写字母开头,可以保证 json.Marshal 的时候数据持久化正确。如果结构体中的变量以小写字母开头,则使得 json.Marshal 的时候忽略该字段,使得该字段的值丢失。从而 json.Unmarshal 的时候将该变量的值置为默认值。如果不加 json 命名,序列化时会使用默认的大写方式。

// 正例
type Position struct {
    X int `json:"x"`
    Y int `json:"y"`
    Z int `json:"z"`
}
type Student struct {
    Name string `json:"name"`
    Sex string `json:"sex"`
    Age int `json:"age"`
    Posi Position `json:"position"`
}
// 反例 序列化,但是不指定json命名 
type Position struct {
    X int
    Y int
    Z int
}
type Student struct {
    Name string
    Sex string
    Age int
    Pos Position
}
  • bool 类型变量直接应直接进行真假判断,避免与 truefalse 比较
**// 正例
**if flag  {
  // do something
}
if !flag  {
   // do something
}
**// 反例
**if flag == true {
}
if flag == false {
}
  • for 循环的嵌套层级不允许超过 3 层。

说明:多层嵌套不利于代码阅读且会导致性能低下。

  • 避免使用 new 关键词创建变量。
// 反例
s := new(MyStruct)

// 正例
s := &MyStruct{}

1.3 错误处理

原则:

  • 异常和错误处理是大部分程序员的痛点,谨慎对待错误,不应该丢弃,当前方法处理不了的,向上层抛出

可参考:handle-errors 🔗

  • 优先使用 errors.New 来创建错误变量,如果有格式化需求,可以使用 fmt.Errorf
  • fmt.Errorf 中使用 : %w 关键字来将一个错误 wrap 至其错误链中
// 正例
list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles"))
2if err != nil {
3    return fmt.Errorf("reading srcfiles list: %w", err)
4}
  • 使用 errors.Is 而非 == 来判定一个错误是否为特定错误。

说明:不同于使用 ==,使用该方法可以判定错误链上的所有错误是否含有特定的错误。

  • 在错误链上获取特定种类的错误,使用 errors.As
  • 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
  • 如果需要在 if 之外使用函数调用的结果,则应采用下面的方式。
// bad
if v, err := foo(); err != nil {
    // error handling
}

// good
v, err := foo()
if err != nil {
    // error handling
}
  • 错误要单独判断,不与其他逻辑组合判断。
// 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")
}
  • 如果返回值需要初始化,则采用下面的方式。
v, err := f()
if err != nil {
    // error handling
    return // or continue.
}
  • 如果某个错误信息可能被多处用到,建议统一整理,避免散乱在代码各处
_var _(
   ErrUnrecognizedType            = errors.New("unrecognized type")
   ErrShouldNotSharding           = errors.New("should not sharding")
   ErrInvalidSQLStatment          = errors.New("invalid sql statement")
   ErrUnsupportedSQLSubQuery          = errors.New("unsupported sub-query")
   ErrUnsupportedSQLJoin          = errors.New("unsupported join")
)
  • 当某种错误多次出现或者需要在其他地方捕获时,我们需要显式地申明这个变量

    • 如果某文件仅存在一个错误,将错误变量放入该代码块开头。
    • 如果某文件存在多个错误,当文件代码量较小的时候,放在文件的开头位置;当文件代码量较大时,不同种类的错误放在相应的代码块开头。
    • 多个错误变量的声明应该与普通变量分隔开。
    • 业务代码的错误可以单独存放在一个文件或者包中。
  • 错误变量名统一以 err 开头,如果需要暴露给外部使用,则以 Err 开头。

  • 错误描述建议

    • 错误描述用小写字母开头,结尾不要加标点符号,例如:

// bad errors.New(“Redis connection failed”) errors.New(“redis connection failed.”)

// good
errors.New("redis connection failed")
	- 错误内容不要出现非 ASCII 编码。
	- 告诉用户他们可以做什么,而不是告诉他们不能做什么。
	- 当声明一个需求时,用 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)`。

### 1.4 recover 使用

- `recover()` 只能在被 `defer` 的函数中使用(嵌套无法生效),并且只在当前 goroutine 生效

- 如果需要更多的上下文信息,可以 `recover()` 后在 log 中记录当前的调用栈。

```go
// 正例
defer func () {
2  if err := recover(); err != nil && err != ErrAbortHandler {
3    const size = 64 << 10
4    buf := make([]byte, size)
5    buf = buf[:runtime.Stack(buf, false)]
6    c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
7  }
8  if !c.hijacked() {
9      c.close()
10      c.setState(c.rwc, StateClosed)
11  }
12}()

1.5 panic 处理

  • 除在影响系统启动等关键步骤中可以 panic 之外,避免使用 panic。 更不要一遇到 error 就 panic,应该处理 error。
// 反例
if err !=nil {
  panic("xxx")
}
// 正例
if err != nil {
  // error handling
}
  • 在 main 包中,只有当程序完全不可运行时使用 panic,例如无法打开文件、无法连接数据库导致程序无法正常运行。
  • 在 main 包中,使用 log.Fatal 来记录错误,这样就可以由 log 来结束程序,或者将 panic 抛出的异常记录到日志文件中,方便排查问题。
  • 可导出的接口一定不能有 panic。
  • 包内建议采用 error 而不是 panic 来传递错误。

1.6 单元测试

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

1.7 类型断言失败处理

  • type assertion 的单个返回值针对不正确的类型将产生 panic。请始终使用 “comma ok”的惯用法。
// bad
t := n.(int)

// good
t, ok := n.(int)
if !ok {
    // error handling
}

2. 命名规范

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

2.1 包命名

  • 包名必须和目录名一致,尽量采取有意义、简短的包名,不要和标准库冲突。

提示:作为例外,我们可以使用 foo_test 作为测试代码的包名。

  • 包名全部小写,没有大写或下划线,使用多级目录来划分层级。
  • 项目名可以通过中划线来连接多个单词。
  • 包名以及包所在的目录名,不要使用复数,例如,是 net/url,而不是 net/urls
  • 不要用 commonutilshared 或者 lib 这类宽泛的、无意义的包名。
  • 包名要简单明了,例如 nettimelog
  • 不使用常用名或标准库作为包名
// 反例
package buf
2package util
3package strings

// 正例
package bufio

2.2 函数命名

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

说明:

  • 因为包名和函数名总是成对出现的,两者所携带的信息不必重叠。对 struct 和 interface 不做此要求。
  • 当名为 foo 的包某个函数返回类型 Foo 时,往往可以省略类型信息而不导致歧义。例如使用 time.Now() 以及 time.Parse(),两者返回的都是 time.Time 类型。

2.3 文件命名

  • 文件名要简短有意义。

  • ** **源码文件命名,统一采用小写, 如有必要可以增加下划线来隔开,避免大写,驼峰等。

BaseApiType.go -> base_api_type.go fooBar.go -> foo_bar.go Service.go -> service.go


### 2.4 结构体命名

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

- 结构体名不应该是动词,应该是名词或名词短语,比如 `Node`、`NodeSpec`。

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

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

```go
// User 多行声明
type User struct {
    Name  string
    Email string
}

// 多行初始化
u := User{
    UserName: "colin",
    Email:    "colin404@foxmail.com",
}

2.5 接口命名

  • 接口命名的规则,基本和结构体命名规则保持一致:
    • 单个函数的接口名以 er 作为后缀(例如 ReaderWriter),有时候可能导致蹩脚的英文,但是没关系。
    • 两个函数的接口名以两个函数名命名,例如 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.6 变量命名

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

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

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

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

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

// 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 类型,则名称应以 HasIsCanAllow 开头,例如:
var hasConflict bool
var isExist bool
var canManage bool
var allowGitHook bool
  • 局部变量应当尽可能短小,比如使用 buf 指代 buffer,使用 i 指代 index。
  • 代码生成工具自动生成的代码可排除此规则(如 xxx.pb.go 里面的 Id)

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 Error 的命名

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

2.9 其他命名规范

  • 方法接收者避免使用 meselfthis 等其它语言保留词, 同时对于同一个方法接收者的简写描述在所有地方都一致
// 反例
func (**this** *ConditionChain) Or(c Condition) {
 **this**.OrItems = append(**this**.OrItems, c)
}
func (self *Person) Say(s string ) {
 **self**.Content = append(**self**.Content, s)
}

// 正例
func (**c** *ConditionChain) Or(cond Condition) {
 **c**.OrItems = append(**c**.OrItems, cond)
}
func (p *Person) Say(s string ) {
 **p**.Content = append(**p**.Content, s)
}
  • 方法接收者一般使用 1 到 2 个字母的缩写代表其原来的类型,比如类型为 Client,可以使用 ccl 等。
  • 使用统一缩略语,并和业界常用的缩略语保持一致,例如:
temp => tmp
flag => flg
statistic => stat
increment => inc
message => msg
buffer => buf
error => err
argument => arg
parameters => params
initialize => init
index => idx
context => ctx
clear => clr
pointer => ptr
previous => prev
request => req
response => resp|rsp
maximum => max
minimum => min
result => ret/res
  • 如果模块,接口,方法等使用了设计模式,应该在命名时体现出具体的模式
type OrderFactory interface {
}
type LoginProxy interface {
}

说明:将设计模式提现在命名中,有助于阅读者快速理解架构设计理念

3. 注释规范

原则:对于注释,能够准确反映设计思想和代码逻辑,描述业务含义,使得其他人能够快速了解代码背景。一个模块中,统一使用中文或者英文,避免中英混搭;避免“中式英语”。

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

说明:对于可以导出的名称,建议都加上注释,这样在使用 godoc 的时候,可以清晰地生成项目文档。

  • 全部使用单行注释(// Comments),禁止使用多行注释。只有大段注释或者 package 头部版权声明时使用 /* */ 格式。
  • 和代码的规范一样,单行注释不要过长,禁止超过 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
}
  • 所有注释掉的代码在提交 code review 前都应该被删除,否则应该说明为什么不删除,并给出后续处理建议。
  • 在多段注释之间可以使用空行分隔加以区分,如下所示:
// 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

3.1 包注释

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

3.2 变量/常量注释

  • 每个可导出的变量/常量都必须有注释说明,格式为// 变量名 变量描述,例如:
// 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
)

3.3 结构体注释

  • 每个需要导出的结构体或者接口都必须有注释说明,格式为 // 结构体名 结构体描述.
  • 结构体内的可导出成员变量名,如果意义不明确,必须要给出注释,放在成员变量的前一行或同一行的末尾。例如:
// 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.4 方法注释

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

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

3.5 类型注释

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

4. 类型

4.1 字符串

  • 空字符串判断。
// bad
if s == "" {
    // normal code
}

// good
if len(s) == 0 {
    // normal code
}
  • []byte/string 相等比较。
// 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
  • 复杂字符串使用 raw 字符串避免字符转义。
// bad
regexp.MustCompile("\\.")

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

4.2 切片

  • 空 slice 判断。
// bad
if len(slice) = 0 {
    // normal code
}

// good
if slice != nil && len(slice) == 0 {
    // normal code
}

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

  • 声明 slice。
// bad
s := []string{}
s := make([]string, 0)

// good
var s []string
  • 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 新增。
// bad
var a, b []int
for _, v := range a {
    b = append(b, v)
}

// good
var a, b []int
b = append(b, a...)

4.3 结构体

  • struct 初始化。

Struct 以多行格式初始化。

type user struct {
    Id   int64
    Name string
}

u1 := user{100, "Colin"}

u2 := user{
    Id:   200,
    Name: "Lex",
}

5. 控制结构

5.1 if

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

5.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
    }()
}

5.3 range

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

5.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)
}

5.5 goto

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

6. 函数与方法

原则:

  • 函数命名一般以它” 做什么” 来命名,而不是以它” 怎么做” 来命名;
  • 单一职责:函数应该只做一件事,并做好这件事。
  • 传入变量和返回变量以小写字母开头。
  • 函数参数入参不超过 5 个,出参不超过 3 个。

说明:过多的出参入参,难以理解,不利于调用。

  • 函数分组与顺序。
  • 函数应按粗略的调用顺序排序。
  • 同一文件中的函数应按接收者分组。
  • 尽量采用值传递,而非指针传递。
  • 传入参数是 map、slice、chan、interface ,不要传递指针。
  • 函数体总行数不要超过一屏可展示的范围,不要超过 80 行,注释,方法签名除外。

说明:过长的方法体不易于理解,应该抽出主干逻辑,分清主干和辅助。

  • 方法参数当传递的值为较大的结构体时,应传递指针,避免底层复制。

说明:参数是 map,slice,chan 不要传递指针,因为 map,slice,chan 是引用类型,当不需要修改 map,slice 内容时不要传递指针的指针。

  • ** **避免没有变量名的 return 语句,不易阅读且容易出错。
// 反例
func run() (n int, err error) {
    // ...
    return
}
// 正例
func run() (n int, err error) {
    // ...
    return n, err
}

6.1 函数参数

  • 如果函数返回相同类型的两个或三个参数,或者如果从上下文中不清楚结果的含义,使用命名返回,其他情况不建议使用命名返回,例如:
func coordinate() (x, y float64, err error) {
    // normal code
}

说明:避免返回命名参数,在函数内部定义变量并返回,表意更明确。 但是当存在多个相同类型的返回参数时,设置有意义的命名,有时候更清晰。

  • 传入变量和返回变量都以小写字母开头。
  • 尽量用值传递,非指针传递。
  • 参数数量均不能超过 5 个。
  • 多返回值最多返回三个,超过三个请使用 struct。

6.2 defer

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

defer resp.Body.Close()

6.3 方法的接收器

  • 推荐以类名第一个英文首字母的小写作为接收器的命名。
  • 接收器的命名在函数超过 20 行的时候不要用单字符。
  • 接收器的命名不能采用 methisself 这类易混淆名称。
  • 方法接收者是指针还是值类型取决于是否需要对接收者本身进行更改。
func(w Win) Foo(p Param) int     **// **w 不会有任何改变
func(w *****Win) Foo(p Param) int    **// **w 会改变数据

6.4 嵌套

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

6.5 变量命名

  • 变量声明尽量放在变量第一次使用的前面,遵循就近原则。
  • 如果魔法数字出现超过两次,则禁止使用,改用一个常量代替,例如:
// PI ...
const Prise = 3.14

func getAppleCost(n float64) float64 {
    return Prise * n
}

func getOrangeCost(n float64) float64 {
    return Prise * n
}

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. 代码可维护及可扩展性约束

  • 尽量避免使用 init 函数,如果要用要确保 init 函数每个文件最多只有一个,每个包只有一个。

说明:不应该依赖于 Go 本身的 init 机制,散乱的 init 对新人非常容易误会。显式的初始化某些模块更容易让开发者知道自己正在做的事情。

  • 禁止 package 在 init() 中调用可能发生错误的行为(例如初始化资源连接)。建议 package 提供显式的初始化函数(例如 Init()),用户自行调用并处理错误。
  • iota 只用于定义系统内部常量,不建议在业务代码中使用
  • 枚举类型需要在 const 组中声明,被赋值的变量应该使用自定义类型,每个 iota 只在某个 const 组中自动枚举,如果定义在新的 const 组中,每次都会从 0 重新开始。
// 正例
type SameSite int
2const (
3    SameSiteDefaultMode SameSite = iota + 1
4    SameSiteLaxMode
5    SameSiteStrictMode
7)

10. 包设计与管理

  • 以 package(包)的形式组织代码,每个 package 有若干 .go 文件组成,每个 go 文件内部应依次按照如下顺序进行组织:
包声明
包导入
常量定义
全局变量定义
类型定义
方法/函数定义  (package级别方法在前,类型方法在后,公有方法在前,私有方法在后)
  • 开发过程中主要会引用到的包类型有:标准库,第三方,程序内部。 采用如下方式进行组织,有顺序的引入包,不同的类型采用空行分离,第一种是标准库,第二是第三方包,第三是项目包,
// 示例
**import (
    "context"
    "fmt"

    "github.com/distribution/reference"
    apierrors "k8s.io/apimachinery/pkg/api/errors"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/util/validation/field"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/webhook"
    "sigs.k8s.io/controller-runtime/pkg/webhook/admission"

    known "github.com/superproj/onex/internal/pkg/known/controllermanager"
    "github.com/superproj/onex/pkg/apis/apps/v1beta1"
****)**

11. 测试

  • 遵守 Go 的单测规范,例如测试文件名以 待测试文件_test.go 命名, 测试方法以 Test 待测试方法_条件_ _命名
  • 测试文件要么放在和待测试文件同一目录下,要么在一个项目中统一放到 tests 的 package 下
  • 建议利用 testify 做断言
// 反例
func TestAdd(t *testing.T) {
    actual := 2 + 2
    expected := 4
    if (actual != expected) {
        t.Errorf("Expected %d, but got %d", expected, actual)
    }
}

// 正例
import "github.com/stretchr/testify/assert"
func TestAdd(t *testing.T) {
    actual := 2 + 2
    expected := 4
    assert.Equal(t, expected, actual)
}
  • ** **单测应该保证测试粒度足够小,有助于精准定位问题,并且单测应该是可重复执行的,不能受外界环境的影响
  • 编写单测时应该遵循 BCE 原则:

** B: Boder**, 边界值测试,包含循环边界,特殊值,特殊时间点等

** C: Correct**, 正确的输入,并得到预期的结果

** E: Error,**强制输入错误数据,例如非法参数,非业务允许输入等,并得到预期结果

12. 最佳实践

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

12.1 性能

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

12.2 注意事项

  • append 要小心自动分配内存,append 返回的可能是新分配的地址。
  • 如果要直接修改 map 的 value 值,则 value 只能是指针,否则要覆盖原来的值。
  • map 在并发中需要加锁。
  • 编译过程无法检查 interface{} 的转换,只能在运行时检查,小心引起 panic。
  • 在高并发场景下,禁止使用==判断作为中断或者退出条件。

说明:如果并发没有控制好,容易产生等值判断被击穿的情况,应该使用大于或者小于的区间判断条件,例如并发情况下,库存为 0 时终止发送奖品,但因为并发处理错误导致库存变为了负数。

  • 使用 context.Context 作为方法或者函数参数时,必须是第一个参数,不能使用结构体存储 context。

总结

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

其他参考

lomtom

标题:最全Go编码规范

转载:https://feizaonet.feishu.cn/docx/HgjGdU8eeoKTJjxOvN8cM6ebnff