Go 代码开发规范
在Go 项目开发中,一个好的编码规范可以极大的提高代码质量。为了帮你节省时间和精力,这里我整理了一份清晰、可直接套用的 Go 编码规范,供你参考。
这份规范,是我参考了 Go 官方提供的编码规范,以及 Go 社区沉淀的一些比较合理的规范之后,加入自己的理解总结出的,它比很多公司内部的规范更全面,你掌握了,以后在面试大厂的时候,或者在大厂里写代码的时候,都会让人高看你一眼,觉得你code很专业。
这份编码规范中包含代码风格、命名规范、注释规范、类型、控制结构、函数、GOPATH 设置规范、依赖管理和最佳实践九类规范。如果你觉得这些规范内容太多了,看完一遍也记不住,这完全没关系。你可以多看几遍,也可以在用到时把它翻出来,在实际应用中掌握。这篇特别放送的内容,更多是作为写代码时候的一个参考手册。
1. 代码风格
1.1 代码格式
gofmtgoimportsgoimportsimport ../util/net
// bad"github.com/dgrijalva/jwt-go/v4"//goodjwt "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 声明、初始化和定义
varvar:=
var (Width intHeight int)
&T{}new(T)
// badsptr := new(T)sptr.Name = "bar"// goodsptr := &T{Name: "bar"}
- struct 声明和初始化格式采用多行,定义如下。
type User struct{Username stringEmail string}user := User{Username: "belm",Email: "nosbelm@qq.com",}
- 相似的声明放在一组,同样适用于常量、变量和类型声明。
// badimport "a"import "b"// goodimport ("a""b")
- 尽可能指定容器容量,以便为容器预先分配内存,例如:
v := make(map[int]string, 4)v := make([]string, 0, 4)
- 在顶层,使用标准var关键字。请勿指定类型,除非它与表达式的类型不同。
// badvar _s string = F()func F() string { return "A" }// goodvar _s = F()// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型// 还是那种类型func F() string { return "A" }
_
// badconst (defaultHost = "127.0.0.1"defaultPort = 8080)// goodconst (_defaultHost = "127.0.0.1"_defaultPort = 8080)
- 嵌入式类型(例如 mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。
// badtype Client struct {version inthttp.Client}// goodtype Client struct {http.Clientversion int}
1.3 错误处理
errorerrordefer xx.Close()
func load() error {// normal code}// badload()// good_ = load()
errorerror
// badfunc load() (error, int) {// normal code}// goodfunc load() (int, error) {// normal code}
- 尽早进行错误处理,并尽早返回,减少嵌套。
// badif err != nil {// error code} else {// normal code}// goodif err != nil {// error handlingreturn err}// normal code
- 如果需要在 if 之外使用函数调用的结果,则应采用下面的方式。
// badif v, err := foo(); err != nil {// error handling}// goodv, err := foo()if err != nil {// error handling}
- 错误要单独判断,不与其他逻辑组合判断。
// badv, err := foo()if err != nil || v == nil {// error handlingreturn err}// goodv, err := foo()if err != nil {// error handlingreturn err}if v == nil {// error handlingreturn errors.New("invalid value v")}
- 如果返回值需要初始化,则采用下面的方式。
v, err := f()if err != nil {// error handlingreturn // or continue.}
- 错误描述建议
- 错误描述用小写字母开头,结尾不要加标点符号,例如:
// baderrors.New("Redis connection failed")errors.New("redis connection failed.")// gooderrors.New("redis connection failed")- 告诉用户他们可以做什么,而不是告诉他们不能做什么。- 当声明一个需求时,用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 panic处理
log.Fatal
1.5 单元测试
example_test.gofunc (b *Bar) Foofunc TestBar_Foo
1.6 类型断言失败处理
- type assertion 的单个返回值针对不正确的类型将产生 panic。请始终使用 “comma ok”的惯用法。
// badt := n.(int)// goodt, ok := n.(int)if !ok {// error handling}
2. 命名规范
命名规范是代码规范中非常重要的一部分,一个统一的、短小的、精确的命名规范可以大大提高代码的可读性,也可以借此规避一些不必要的Bug。
2.1 包命名
net/urlnet/urls
2.2 函数命名
MixedCapsmixedCapsxxxx.pb.goTestMyFunction_WhatIsBeingTested
2.3 文件命名
- 文件名要简短有意义。
- 文件名应小写,并使用下划线分割单词。
2.4 结构体命名
MixedCapsmixedCapsNodeNodeSpec
// User 多行声明type User struct {Name stringEmail string}// 多行初始化u := User{UserName: "belm",Email: "nosbelm@qq.com",}
2.5 接口命名
- 接口命名的规则,基本和结构体命名规则保持一致:
- 单个函数的接口名以 “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 {ReaderWriter}
2.6 变量命名
- 变量名必须遵循驼峰式,首字母根据访问控制决定使用大写或小写。
- 在相对简单(对象数量少、针对性强)的环境中,可以将一些名称由完整单词简写为单个字母,例如:
- user 可以简写为 u;
- userID 可以简写 uid。
- 特有名词时,需要遵循以下规则:
- 如果变量为私有,且特有名词为首个单词,则使用小写,如 apiClient。
- 其他情况都应当使用该名词原有的写法,如 APIClient、repoID、UserID。
下面列举了一些常见的特有名词。
// A GonicMapper that contains a list of common initialisms taken from golang/lintvar 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 boolvar isExist boolvar canManage boolvar allowGitHook bool
xxx.pb.go
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")
3. 注释规范
格式为 // 名称 描述.
// 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 codereturn nil}
3.5 类型注释
// 类型名 类型描述.
// Code defines an error code type.type Code int
4. 类型
4.1 字符串
- 空字符串判断。
// badif s == "" {// normal code}// goodif len(s) == 0 {// normal code}
[]bytestring
// badvar s1 []bytevar s2 []byte...bytes.Equal(s1, s2) == 0bytes.Equal(s1, s2) != 0// goodvar s1 []bytevar s2 []byte...bytes.Compare(s1, s2) == 0bytes.Compare(s1, s2) != 0
- 复杂字符串使用raw字符串避免字符转义。
// badregexp.MustCompile("\\.")// goodregexp.MustCompile(`\.`)
4.2 切片
- 空slice判断。
// badif len(slice) = 0 {// normal code}// goodif slice != nil && len(slice) == 0 {// normal code}
上面判断同样适用于map、channel。
- 声明slice。
// bads := []string{}s := make([]string, 0)// goodvar s []string
- slice复制。
// badvar b1, b2 []bytefor i, v := range b1 {b2[i] = v}for i := range b1 {b2[i] = b1[i]}// goodcopy(b2, b1)
- slice新增。
// badvar a, b []intfor _, v := range a {b = append(b, v)}// goodvar a, b []intb = append(b, a...)
4.3 结构体
- struct初始化。
struct以多行格式初始化。
type user struct {Id int64Name string}u1 := user{100, "Colin"}u2 := user{Id: 200,Name: "Lex",}
5. 控制结构
5.1 if
- if 接受初始化语句,约定如下方式建立局部变量。
if err := loadConfig(); err != nil {// error handlingreturn err}
- if 对于bool类型的变量,应直接进行真假判断。
var isAllow boolif isAllow {// normal code}
5.2 for
- 采用短声明建立局部变量。
sum := 0for i := 0; i < 10; i++ {sum += 1}
- 不要在 for 循环里面使用 defer,defer只有在函数退出时才会执行。
// badfor file := range files {fd, err := os.Open(file)if err != nil {return err}defer fd.Close()// normal code}// goodfor 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 := 0for _, 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个。
- 函数分组与顺序
- 函数应按粗略的调用顺序排序。
- 同一文件中的函数应按接收者分组。
- 尽量采用值传递,而非指针传递。
- 传入参数是 map、slice、chan、interface ,不要传递指针。
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行的时候不要用单字符。
- 接收器的命名不能采用me、this、self这类易混淆名称。
6.4 嵌套
- 嵌套深度不能超过4层。
6.5 变量命名
- 变量声明尽量放在变量第一次使用的前面,遵循就近原则。
- 如果魔法数字出现超过两次,则禁止使用,改用一个常量代替,例如:
// PI ...const Prise = 3.14func 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. 最佳实践
- 尽量少用全局变量,而是通过参数传递,使每个函数都是“无状态”的。这样可以减少耦合,也方便分工和单元测试。
- 在编译时验证接口的符合性,例如:
type LogHandler struct {h http.Handlerlog *zap.Logger}var _ http.Handler = LogHandler{}
- 服务器处理请求时,应该创建一个context,保存该请求的相关信息(如requestID),并在函数调用链中传递。
9.1 性能
- string 表示的是不可变的字符串变量,对 string 的修改是比较重的操作,基本上都需要重新申请内存。所以,如果没有特殊需要,需要修改时多使用 []byte。
- 优先使用 strconv 而不是 fmt。
9.2 注意事项
- append 要小心自动分配内存,append 返回的可能是新分配的地址。
- 如果要直接修改 map 的 value 值,则 value 只能是指针,否则要覆盖原来的值。
- map 在并发中需要加锁。
- 编译过程无法检查 interface{} 的转换,只能在运行时检查,小心引起 panic。
总结
这里向你介绍了九类常用的编码规范。但今天的最后,我要在这里提醒你一句:规范是人定的,你也可以根据需要,制定符合你项目的规范,但同时我也建议你采纳这些业界沉淀下来的规范,并通过工具来确保规范的执行。