Google Golang 代码规范1. 前言
为形成公司技术团队统一的 Go 编码风格,以保障公司项目代码的易维护性和编码安全性,特制定本规范。
本规范在 Google Golang 代码规范 的基础上,根据业务团队实际情况进行了调整和补充。
每项规范内容,给出了要求等级,其定义为:
必须(Mandatory):用户必须采用; 推荐(Preferable):用户理应采用,但如有特殊情况,可以不采用; 可选(Optional):用户可参考,自行决定是否采用;
2. 代码风格
2.1 【必须】格式化
gofmt2.2 【推荐】换行
120列2.3 【必须】括号和空格
gofmt2.4 【必须】import 规范
goimportsgoimportsgoimportsgoimports// 不要采用这种方式import ( '../net')应该使用完整的路径引入包:
import (
'xxxx.com/proj/net'
)
包名和 git 路径名不一致时,或者多个相同包名冲突时,使用别名代替,别名命名规范和包命名规范保持一致:
// 合理用法:包名和 git 路径名不一致,使用别名import ( opentracing 'github.com/opentracing/opentracing-go')// 合理用法:多个相同包名冲突,使用别名import ( 'fmt' 'os' 'runtime/trace' nettrace 'golang.net/x/trace')// 不合理用法:包名和路径名一致,也不存在多包名冲突,并且原包名符合规范,则不应该使用别名import ( 'fmt' 'os' nettrace 'golang.net/x/trace')【可选】第三方包的包名不符合规范可使用别名修正:
import (
xyz 'github.com/xxxx/X_Y_Z'
)
【可选】匿名包的引用建议使用一个新的分组引入,并在匿名包上写上注释说明。
完整示例如下:
import ( // standard package & inner package 'encoding/json' 'myproject/models' 'myproject/controller' 'strings' // third-party package 'github.com/obc/utils' 'github.com/dep/beego' 'github.com/dep/mysql' opentracing 'github.com/opentracing/opentracing-go' // anonymous import package // import filesystem storage driver _ 'github.com/org/repo/pkg/storage/filesystem)例外包 embed 当使用 embed 内嵌外部数据时不需要注释
2.5 【必须】错误处理
2.5.1 【必须】error 处理
errorerrordefer xx.Close()errorerror// 不要采用这种方式
func do() (error, int) {
}
// 要采用下面的方式
func do() (int, error) {
}
错误描述不需要标点结尾。
采用独立的错误流进行处理。
// 不要采用这种方式if err != nil { // error handling} else { // normal code}// 而要采用下面的方式if err != nil { // error handling return // or continue, etc.}// normal code如果返回值需要初始化,则采用下面的方式:
x, err := f()
if err != nil {
// error handling
return // or continue, etc.
}
// use x
错误返回的判断独立处理,不与其他变量组合逻辑判断。
// 不要采用这种方式:x, y, err := f()if err != nil || y == nil { return err // 当y与err都为空时,函数的调用者会出现错误的调用逻辑}// 应当使用如下方式:x, y, err := f()if err != nil { return err}if y == nil { return errors.New('some error')}errors.New('xxxx')fmt.Errorf('module xxx: %w', err)2.5.2 【必须】panic 处理
panicmainpanicpanicpanicerror// 不推荐为传递error而在包内使用panic,以下为示例
// PError 包内定义的错误类型
type PError string
// Error error接口方法
func (e PError) Error() string {
return string(e)
}
func do(str string) {
// ...
// 此处的panic用于传递error
panic(PError('错误信息'))
// ...
}
// Do 包级访问入口
func Do(str string) error {
var err error
defer func() {
if e := recover(); e != nil {
err = e.(PError)
}
}()
do(str)
return err
}
mainlog.Fatallogpanicpanicgoroutinegoroutinepanic2.5.3 【必须】recover 处理
recoverruntimerecoverdeferpanicpanicpackage mainimport ( 'log')func main() { defer func() { if err := recover(); err != nil { // do something or record log log.Println('exec panic error: ', err) // log.Println(debug.Stack()) } }() getOne() panic(11) // 手动抛出panic}// getOne 模拟slice越界 runtime运行时抛出的panicfunc getOne() { defer func() { if err := recover(); err != nil { // do something or record log log.Println('exec panic error: ', err) // log.Println(debug.Stack()) } }() var arr = []string{'a', 'b', 'c'} log.Println('hello,', arr[4])}// 执行结果:// 2020/01/02 17:18:53 exec panic error: runtime error: index out of range// 2020/01/02 17:18:53 exec panic error: 112.6 【必须】单元测试
example_test.goTestTestExamplefunc Foofunc Test_Foofunc (b *Bar) Foofunc TestBar_Foo1600行160行2.7 【必须】类型断言失败处理
type assertionpanic“comma ok”// 不要采用这种方式
t := i.(string)
// 而要采用下面的方式
t, ok := i.(string)
if !ok {
// 优雅地处理错误
}
3. 注释
godoc3.1 【必须】包注释
每个包都应该有一个包注释。
包如果有多个 go 文件,只需要出现在一个 go 文件中(一般是和包同名的文件)即可,格式为:“// Package 包名 包信息描述”。
// Package math provides basic constants and mathematical functions.package math// 或者/*Package template implements data-driven templates for generating textualoutput such as HTML.....*/package template3.2 【必须】结构体注释
每个需要导出的自定义结构体或者接口都必须有注释说明。
注释对结构进行简要介绍,放在结构体定义的前一行。
格式为:'// 结构体名 结构体信息描述'。
结构体内的可导出成员变量名,如果是个生僻词,或者意义不明确的词,就必须要给出注释,放在成员变量的前一行或同一行的末尾。
// User 用户结构定义了用户基础信息
type User struct {
Name string
Email string
// Demographic 族群
Demographic string
}
3.3 【必须】方法注释
每个需要导出的函数或者方法(结构体或者接口下的函数称为方法)都必须有注释。注意,如果方法的接收器为不可导出类型,可以不注释,但需要质疑该方法可导出的必要性。
注释描述函数或方法功能、调用方等信息。
格式为:'// 函数名 函数信息描述'。
// NewtAttrModel 是属性数据层操作类的工厂方法func NewAttrModel(ctx *common.Context) *AttrModel { // TODO}例外方法: Write Read 用于常见 IO ServeHTTP 用于 HTTP 服务 String 用于打印 Unwrap Error 用于错误处理 Len Less Swap 用于排序
3.4 【必须】变量和常量注释
每个需要导出的常量和变量都必须有注释说明。
该注释对常量或变量进行简要介绍,放在常量或者变量定义的前一行。
大块常量或变量定义时,可在前面注释一个总的说明,然后每一行常量的末尾详细注释该常量的定义。
格式为:'// 变量名 变量信息描述',斜线后面紧跟一个空格。
// FlagConfigFile 配置文件的命令行参数名
const FlagConfigFile = '--config'
// 命令行参数
const (
FlagConfigFile1 = '--config' // 配置文件的命令行参数名1
FlagConfigFile2 = '--config' // 配置文件的命令行参数名2
FlagConfigFile3 = '--config' // 配置文件的命令行参数名3
FlagConfigFile4 = '--config' // 配置文件的命令行参数名4
)
// FullName 返回指定用户名的完整名称
var FullName = func(username string) string {
return fmt.Sprintf('fake-%s', username)
}
3.5 【必须】类型注释
每个需要导出的类型定义(type definition)和类型别名(type aliases)都必须有注释说明。
该注释对类型进行简要介绍,放在定义的前一行。
格式为:'// 类型名 类型信息描述'。
// StorageClass 存储类型type StorageClass string// FakeTime 标准库时间的类型别名type FakeTime = time.Time4. 命名规范
命名是代码规范中很重要的一部分,统一的命名规范有利于提高代码的可读性,好的命名仅仅通过命名就可以获取到足够多的信息。
4.1 【推荐】包命名
packagetimelisthttputilcommonmiscglobalpackagecommonxx/util/encryption4.2 【必须】文件命名
采用有意义,简短的文件名。
文件名应该采用小写,并且使用下划线分割各个单词。
4.3 【必须】结构体命名
CustomerWikiPageAccountAddressParserDataInfo// User 多行声明
type User struct {
Name string
Email string
}
// 多行初始化
u := User{
Name: 'john',
Email: 'john@example.com',
}
4.4 【推荐】接口命名
erReaderWriter// Reader 字节数组读取接口type Reader interface { // Read 读取整个给定的字节数据并返回读取的长度 Read(p []byte) (n int, err error)}两个函数的接口名综合两个函数名。
三个以上函数的接口名,类似于结构体名。
// Car 小汽车结构申明
type Car interface {
// Start ...
Start([]byte)
// Stop ...
Stop() error
// Recover ...
Recover()
}
4.5 【必须】变量命名
apiClientAPIClientrepoIDUserIDUrlArrayurlArrayURLArrayclineCountisliceIndex4.6 【必须】常量命名
常量均需遵循驼峰式。
// AppVersion 应用程序版本号定义const AppVersion = '1.0.0'如果是枚举类型的常量,需要先创建相应类型:
// Scheme 传输协议
type Scheme string
const (
// HTTP 表示HTTP明文传输协议
HTTP Scheme = 'http'
// HTTPS 表示HTTPS加密传输协议
HTTPS Scheme = 'https'
)
私有全局常量和局部变量规范一致,均以小写字母开头。
const appVersion = '1.0.0'4.7 【必须】函数命名
函数名必须遵循驼峰式,首字母根据访问控制决定使用大写或小写。 代码生成工具自动生成的代码可排除此规则(如协议生成文件 xxx.pb.go , gotests 自动生成文件 xxx_test.go 里面的下划线)。
5. 控制结构
5.1 【推荐】if
ifif err := file.Chmod(0664); err != nil {
return err
}
if// 不要采用这种方式if nil != err { // error handling}// 不要采用这种方式if 0 == errorCode { // do something}// 而要采用下面的方式if err != nil { // error handling}// 而要采用下面的方式if errorCode == 0 { // do something}ifvar allowUserLogin bool
// 不要采用这种方式
if allowUserLogin == true {
// do something
}
// 不要采用这种方式
if allowUserLogin == false {
// do something
}
// 而要采用下面的方式
if allowUserLogin {
// do something
}
// 而要采用下面的方式
if !allowUserLogin {
// do something
}
5.2 【推荐】for
采用短声明建立局部变量:
sum := 0for i := 0; i < 10; i++ { sum += 1}5.3 【必须】range
如果只需要第一项(key),就丢弃第二个:
for key := range m {
if key.expired() {
delete(m, key)
}
}
如果只需要第二项,则把第一项置为下划线:
sum := 0for _, value := range array { sum += value}5.4 【必须】switch
defaultswitch os := runtime.GOOS; os {
case 'darwin':
fmt.Println('OS X.')
case 'linux':
fmt.Println('Linux.')
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf('%s.\n', os)
}
5.5 【推荐】return
returnf, err := os.Open(name)if err != nil { return err}defer f.Close()d, err := f.Stat()if err != nil { return err}codeUsing(f, d)5.6 【必须】禁止 goto
goto6. 函数
6.1 【推荐】函数参数
函数返回相同类型的两个或三个参数,或者如果从上下文中不清楚结果的含义,使用命名返回,其它情况不建议使用命名返回。
// Parent1 ...
func (n *Node) Parent1() *Node
// Parent2 ...
func (n *Node) Parent2() (*Node, error)
// Location ...
func (f *Foo) Location() (lat, long float64, err error)
5个mapslicechaninterface6.2 【必须】defer
deferdeferresp, err := http.Get(url)if err != nil { return err}// 如果操作成功,再defer Close()defer resp.Body.Close()defer// 不要这样使用
func filterSomething(values []string) {
for _, v := range values {
fields, err := db.Query(v) // 示例,实际不要这么查询,防止sql注入
if err != nil {
// xxx
}
defer fields.Close()
// 继续使用fields
}
}
// 应当使用如下的方式:
func filterSomething(values []string) {
for _, v := range values {
func() {
fields, err := db.Query(v) // 示例,实际不要这么查询,防止sql注入
if err != nil {
...
}
defer fields.Close()
// 继续使用fields
}()
}
}
6.3 【推荐】方法的接收器
20行methisself6.4 【推荐】代码行数
800行80行6.5 【必须】嵌套
4层// AddArea 添加成功或出错func (s *BookingService) AddArea(areas ...string) error { s.Lock() defer s.Unlock() for _, area := range areas { for _, has := range s.areas { if area == has { return srverr.ErrAreaConflict } } s.areas = append(s.areas, area) s.areaOrders[area] = new(order.AreaOrder) } return nil}// 建议调整为这样:
// AddArea 添加成功或出错
func (s *BookingService) AddArea(areas ...string) error {
s.Lock()
defer s.Unlock()
for _, area := range areas {
if s.HasArea(area) {
return srverr.ErrAreaConflict
}
s.areas = append(s.areas, area)
s.areaOrders[area] = new(order.AreaOrder)
}
return nil
}
// HasArea ...
func (s *BookingService) HasArea(area string) bool {
for _, has := range s.areas {
if area == has {
return true
}
}
return false
}
6.6 【推荐】变量声明
变量声明尽量放在变量第一次使用前面,就近原则。
6.7 【必须】魔法数字
2次func getArea(r float64) float64 { return 3.14 * r * r}func getLength(r float64) float64 { return 3.14 * 2 * r}用一个常量代替:
// PI ...
const PI = 3.14
func getArea(r float64) float64 {
return PI * r * r
}
func getLength(r float64) float64 {
return PI * 2 * r
}
7. 依赖管理
go modulesgo mod init github.com/group/myrepo7.2 【推荐】代码提交
module namegithub.com/group/repogo modulesvendorgo modulesgo.sum8. 应用服务
README.md其中建议包括服务基本描述、使用方法、部署时的限制与要求、基础环境依赖(例如最低 go 版本、最低外部通用包版本)等。
8.2 【必须】应用服务必须要有接口测试。
附:常用工具
go 语言本身在代码规范性这方面也做了很多努力,很多限制都是强制语法要求,例如左大括号不换行,引用的包或者定义的变量不使用会报错,此外 go 还是提供了很多好用的工具帮助我们进行代码的规范。
gofmtgofmtgofmtgofmtgoimportsgofmtgo vetvetreturnstructtaggolintjavascriptjslint