编码规范
1. 大小约定
【建议】单个文件长度尽量不要超过500行
【建议】每个函数长度尽量不要超过50行
【建议】单个函数圈复杂度尽量不要超过10,禁止超过15
【建议】单个行数中嵌套不要超过3层
【建议】单行注释尽量不超过80个字符
【建议】单行语句尽量不超过80个字符
2. 缩进、括号和空格约定
【规则】缩进、括号和空格都使用gofmt工具处理:
强制使用tab缩进
强制左括号不换行
强制所有的运算符和操作数之间要留空格
3 命名规范
3.1 包、目录命名规范
【规则】包名和目录名保持一致。一个目录尽量维护一个包下的所有文件
【规则】包名为全小写单词,不使用复数,不使用下划线
【建议】包名因该尽可能简短
3.2 文件名命名规范
【规则】文件名为全小写单词,使用"_"分词
3.3 标识符命名规范
【建议】短名优先,作用域越大命名越长且越有意义
变量、常量名
【规则】变量命名遵循驼峰法
【规则】常量使用全大写单词,使用"_"分词
【说明】首字母根据访问控制原则使用大写或者小写
【建议】对于常规缩略语,一旦选择了大写或者小写的风格,就应当在整份代码中保持这种风格,不要首字符大写和缩写两种风格混用。
以URL为例,如果选择了URL这种风格,则应在整份代码中保持。
Bad:UrlArray
Good:urlArray或URLArray
再以ID为例,如果选择了缩写ID这种风格:
Bad:appleId
Good:appleID
【规则】对于只在本文中有效的顶级变量、常量,应该使用"_"前缀,避免在同一个包中的其他文件中意外使用错误的值。例如:
var (
_defaultPort = 8080
_defaultUser = "user"
)
【规则】若变量、常量为bool类型,则名称应该以Has、Is、Can或Allow开头
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
【建议】如果模块的功能较为复杂、常量名称容易混淆的情况下,为了更好地区分枚举类型,可以使用完整的前缀
type PullRequestStatus int
const (
PULL_REQUEST_STATUS_CONFLICT PullRequestStatus = iota
PULL_REQUEST_STATUS_CHECKING
PULL_REQUEST_STATUS_MERGEABLE
)
函数名、方法名
【规则】函数、方法(结构体或者接口下属的函数称为方法)命名规则:动词+名词
【规则】若函数、方法判断类型(返回值主要为bool类型),则名称应以Has、Is、Can或Allow等判断动词开头
func HasPrefix(name string, prefixes []string) bool {...}
func IsEntry(name string, entries []string) bool {...}
func CanManage(name string) bool {...}
func AllowGitHook() bool {...}
结构体、接口名
【规则】结构体命名规则:名词或名词短语
【规则】接口命名规则:以"er"作为后缀,例如Reader、Writer。接口实现的方法去掉"er",例如Read、Write
type Reader interface {
Read(p []byte)(n int, err error)
}
// 多个函数接口
type WriteFlusher interface {
Write([]byte)(int, error)
Flush() error
}
3.4 空行、注释、文档规范
空行
【规则】空行需要体现代码逻辑的关联,所以空行不能随意,非常严重地影响可读性
【规则】保持函数内部实现的组织粒度是相似的,用空行分隔
注释与文档
Golang的go doc工具可以根据注释生成代码文档,所以注释的质量决定了代码文档的质量。
注释风格
【建议】统一使用中文注释,中西文之间严格使用空格分隔,严禁使用中文标点符号
【建议】注释应该是一个完整的句子,以句号结尾
【建议】句子类型的注释首字母需大写,短语类型的注释首字母需小写
【规则】注释的单行长度不能超过80个字符
包注释
【建议】每个包都应该有一个包注释。包注释会首先出现在go doc网页上。包注释应该包含:
包名、简介
创建者
创建时间
// Gogs (Go Git Service) is a painless self-hosted Git Service
package main
【说明】对于简单的非main包,也可用一行注释概况
【说明】对于一个复杂项目的子包,一般情况下不需要包级别注释,除非是代表某个特定功能的模块
【建议】对于相对复杂的非main包,一般都会增加一些使用示例或者基本说明,且以Package开头
【建议】对于特别复杂的包说明,一般使用doc.go文件用于编写包的描述,并提供与整个包相关的信息
函数、方法注释
【规则】每个函数、方法(结构体或者接口下属的函数称为方法)都应该有注释说明,包括三个方面(顺序严格):
函数、方法名,简要说明
参数列表,每行一个参数
返回值,每行一个返回值
结构体、接口注释
【建议】每个自定义的结构体、接口都应该有注释说明,放在实体定义的前一行,格式为:名称、说明。同时,结构体内的每个成员都要有说明,该说明放在成员变量的后面(注意对齐),例如:
// User,用户实例,定义了用户的基础信息。
type User struct{
Username string // 用户名
Email string // 邮箱
}
【规则】当某个部分等待完成时,用TODO(Your name):开头的注释来提醒维护人员
【规则】当某个部分存在已知问题需要进行修复或者改进时,用FIXME(Your name):开头的注释来提醒维护人员
【规则】当需要特别说明某个问题时,可用NOTE(Your name):开头的注释
4. 导入规范
使用goimports工具,在保存文件时自动检查import规范。
【规则】如果使用的包没有导入,则自动导入;如果导入的包没有被使用,则自动删除
【规则】强制使用分行导入,即便仅导入一个包
【规则】导入多个包时注意按照顺序并使用空行区分:标准库包、程序内部包、第三方包
【规则】禁止使用相对路径导入
【规则】禁止使用import dot(“.”)简化导入
【规则】在所有其他情况下,除非导入之间有直接冲突,否则应避免导入别名
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
【规则】如果包名导入路径的最后一个元素不匹配,则必须使用导入别名
import (
client "example.com/client-go"
trace "example.com/trace/v2"
)
5.代码逻辑实现规范
5.1 变量、常量定义规范
【规则】函数内使用短变量声明(海象运算符)
【规则】函数外使用长变量声明(var关键字),var关键字一般用于包级别变量声明,或者函数内的零值情况
【规则】变量、常量的分组说明一般需要按照功能来区分,而不是将所有类型都分在一组
const (
// Default section name.
DEFAULT_SECTION = "DEFAULT"
// Maximum allowed depth when recursively substituing variable names.
_DEPTH_VALUES = 200
)
type ParseError int
const (
ERR_SECTION_NOT_FOUND ParseError = iota + 1
ERR_KEY_NOT_FOUND
ERR_BLANK_SECTION_NAME ERR_COULD_NOT_PARSE
)
【规则】如果有可能,尽量缩小变量的作用范围
5.2 String类型定义规范
【规则】声明Print-style String时,将其设置为const常量,这有助于go vet对String类型实例执行静态分析
// Bad
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
// Good
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
【建议】优先使用strconv而不是fmt,将原语转换为字符串或从字符串转换时,strconv速度比fmt快
5.3 Slice、Map类型定义规范
【建议】尽可能指定容器的容量,以便为容器预先分配内存,向make()传入容量参数会在初始化时尝试调整Slice、Map类型实例的大小,这将减少在元素添加到Slice、Map类型实例时的重新分配内存造成的损耗
【说明】使用make()初始化Map类型变量,使得开发者可以很好地区分开Map类型实例的声明,或初始化。使用make()还可以方便地添加大小提示
var (
// m1 读写安全
// m2 在写入时会 panic
m1 = make(map[T1]T2)
m2 map[T1]T2
)
【建议】如果Map类型实例包含固定的元素列表,则使用 map literals(map 初始化列表)
5.4 结构体定义规范
【规则】嵌入结构体中作为成员的结构体,应该位于结构体内成员列表的顶部,并且必须有一个空行将嵌入式成员与常规成员分隔开
【规则】在初始化Struct类型的指针实例时,使用&T{}代替new{T},使其与初始化Struct类型实例一致
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
5.5 接口定义规范
【说明】特别的,如果希望通过接口的方法修改接口实例的实际数据,则必须传递接口实例的指针(将实例指针赋值给接口变量),因为指针指向真正的内存数据
5.6 函数、方法定义规范
【规则】函数、方法的参数排列顺序遵循以下几点原则(从左到右):
参数的重要程度与逻辑顺序
简单类型优先于复杂类型
尽可能将同种类型的参数放在相邻位置,则只需写一次类型
【规则】函数、方法的顺序一般需要按照依赖关系由浅入深由上至下排序,即最底层的函数出现在最前面。例如,函数ExecCmdDirBytes属于最底层的函数,它被ExecCmdDir函数调用,而xecCmdDir又被ExecCmd调用
【建议】避免实参传递时语义不明确,当参数名称的含义不明显时,使用块注释语法
func printInfo(name string, isLocal, done bool)
// Bad
printInfo("foo", true, true)
// Good
printInfo("foo", true /* isLocal /, true / done */)
5.7 函数、方法定义规范
【规则】err总是作为函数返回值列表的最后一个
【规则】如果一个函数return error,一定要检查它是否为空,判断函数调用是否成功。如果不为空,说明发生了错误,一定要处理它
【规则】不能使用_丢弃任何return的err。若不进行错误处理,要么再次向上游return err,或者使用log记录下来
【规则】尽早return err,函数中优先进行return检测,遇见错误马上return err
【建议】错误提示(Error Strings)不需要大写字母开头的单词,即使句子的首字母也不需要。除非那是专有名词或者缩写。同时,错误提示也不需要以句号结尾,因为通常在打印错误提示后还需要跟随别的提示信息
【建议】采用独立的错误流进行处理。尽可能减少正常逻辑代码的缩进,这有利于提高代码的可读性,便于快速分辨出那些还是正常逻辑代码
5.8 单元测试规范
【建议】单元测试都必须使用GoConvey编写,且覆盖率必须在80%以上
【规则】业务代码文件和单元测试文件放在同一目录下
【规则】单元测试文件名以*_test.go为后缀,例如:example_test.go
【规则】测试用例的函数名必须以Test开头,例如:Test_Logger
【规则】如果为结构体的方法编写测试用例,则需要以Test__的形式命名
【建议】每个重要的函数都要同步编写测试用例
【规则】测试用例和业务代码同步提交,方便进行回归测试
【规则】在测试中,我们很可能使用import dot (".") 这个特性,我们可以避免循环引用问题,除此之外都不要使用.进行简易导入
package foo_test
import (
"bar/testutil" // also imports "foo"
. "foo"
)
以上例子,该测试文件不能定义在于 foo 包里面,因为它 import bar/testutil,而 bar/testutil 有 import 了 foo,这将构成循环引用。所以我们需要将该测试文件定义在 foo_test 包中。并且使用 import . “foo” 后,该测试文件内代码能直接调用 foo 里面的函数而不需要显式地写上包名。