Go 代码规范指南

Published on 2019 - 10 - 08

Go 编码规范指南 - 「哈罗单车」

1 - 格式化规范


1.1 - 格式化工具

建议使用goimport工具,这个在gofmt的基础上增加了自动删除和引入包.

1.2 - 行长约定

一行最长不超过80个字符,超过的请使用换行展示,尽量保持格式优雅。

1.3 - 注释

在编码阶段同步写好变量、函数、包注释,注释可以通过godoc导出生成文档。注释必须是完整的句子,以需要注释的内容作为开头,句点作为结尾。程序中每一个被导出的(大写的)名字,都应该有一个文档注释。可以通过 /* …… */ 或者 // ……增加注释, //之后应该加一个空格。

1.3.1 - 包注释

每个程序包都应该有一个包注释,一个位于package子句之前的块注释或行注释。包如果有多个go文件,可以创建一个空的doc.go文件,用来写包注释。

//Package regexp implements a simple library
//for regular expressions.

package regexp

1.3.2 - 可导出类型

第一条语句应该为一条概括语句,并且使用被声明的名字作为开头。

// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {

1.4 - 命名规范

1.4.1 - 包名

包名应为小写单词,不可为复数,不应有下划线或者混合大小写。正确示例 controller, model, router, view

1.4.2 - 变量名命名规范

变量采用驼峰式命名,不能以下划线或美元符号开始,导出变量首字母大写,否则小写

- 全局变量:驼峰式,首字母大写(如果不可导出,则首字母小写)
- 参数传递:驼峰式,首字母小写
- 常量:全部大写,单词以下划线连接

采用全部大写或者全部小写来表示缩写单词,比如对于url这个单词,不要使用UrlPony而要使用urlPony或者 URLPony

1.4.3 - 方法名命名规范

方法采用驼峰式命名,不能以下划线或美元符号开始,导出的方法首字母大写,否则小写

1.4.4 - 接口命名规范

单个函数的接口名以 er 为后缀,如 Reader, Writer,其具体的实现则去掉 er,如

type Reader interface {
    Read(p []byte) (n int, err error)
}

两个函数的接口名综合两个函数名,如

type WriteFlusher interface {
    Write([]byte) (int, error)
    Flush() error
}

三个以上函数的接口名类似于结构体名,如

type Car interface {
    Start([]byte)
    Stop() error
    Recover()
}

1.4.5 - 结构体方法接收参数名

Receiver 的名称应该缩写,一般使用一个或者两个字符作为Receiver的名称,如

// 正确
type User struct{}
func (u *User)Get(){}

// 错误
type User struct{}
func (user *User)Get(){}

另外,当一个结构体函数中的接受者命名要保持一致。上例中,如果使用了u,则不要使用us

1.5 - import 规范

import在多行的情况下,goimports会自动帮你格式化,但是我们这里还是规范一下import的一些规范,如果你在一个文件里面引入了一个package,还是建议采用如下格式:

import (
    "fmt"
)

如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:

import (
    "encoding/json"
    "strings"

    "myproject/models"
    "myproject/controller"

    _ "github.com/go-sql-driver/mysql"
)  

有顺序的引入包,不同的类型采用空格分离,第一种实标准库,第二是项目包,第三是第三方包。

在项目中不要使用相对路径引入包:

// 这是不好的导入
import "../net"

// 这是正确的做法
import github.com/repo/proj/src/net

2 - 变量声明规范

2.1 - 切片声明

// 正确示例
var names []string
// 错误示例
var names = []string{}

前者声明了一个空切片,而后者还分配了内存

2.2 - map声明

声明map时直接初始化,否则无法继续使用

// 正确示例
var cache = map[string]string{}
cache[key] = val
// 错误示例
var cache map[string]string
cache[key] = val // 这里会panic

当只关心map的key而不关心map的value,使用struct{}作为map的value,示例:

var filter = map[string]struct{}{}

if _, exist := filter["key"]; exist {
    log.Println(exist)
}

2.3 - 声明一个零值变量

var name string

2.4 - 声明变量并赋值

局部变量

name := "hello bike"

全局变量

var name = "hello bike"

当类型推断不明确时

var age uint64 = 30

2.5 - 声明枚举类型

枚举类型必须使用自定义类型,不允许直接使用内置类型

示例

type AccountType uint8

const (
    Vip AccountType = iota
    SuperVip
)

func (t AccountType) String() string {
    switch t {
        case Vip:
            return "👑"
        case SuperVip:
            return "super 👑"
    }
    return "unkonw account type"
}

3 - 流程控制

3.1 - switch

禁止使用 fallthrough,每个case必须有明确的处理逻辑

// 正确
switch a {
    case 1, 2:
        doSth(a)
    default:
        doElse(a)
}

// 错误
switch a {
    case 1:
        fallthrough
    case 2:
        doSth(a)
    default:
        doElse(a)
}

3.2 - if 条件判断

在判断异常的分支时,少用if/else方式,尽早return,尽量减少if的分支和缩进

正确的示例:

if err := doA(); err != nil {
    handleErr(err)
    return err
}

if err := doB(); err != nil {
    handleErr(err)
    return err
}
return doC()

错误的示例:

if err := doA(); err == nil {
    if err := doB(); err == nil {
        if err := doC(); err == nil {
            return nil
        } else {
            handleErr(err)
            return err
        }
    } else {
        handleErr(err)
        return err
    }
} else {
    handleErr(err)
    return err
}

3.3 - for 循环

for 采用短声明建立局部变量

var sum int
for i := 0; i < 10; i++ {
    sum += i
}

range 如果只需要第一项(key/index),就丢弃第二个

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

range 如果只需要第二项,则把第一项置为下划线

sum := 0
for _, value := range array {
    sum += value
}

对切片进行range操作时,禁止对循环中的临时变量取地址操作

// 错误操作
var values []int
var ret []*int
for i, v := range values {
    ret = append(ret, &v)
}

// 正确操作
var values []int
var ret []*int
for i, v := range values {
    tmp := v
    ret = append(ret, &tmp)
}

3.4 - defer

defer在函数return之前执行,对于一些资源的回收用defer是好的,但也禁止滥用
defer,defer是需要消耗性能的,所以频繁调用的函数尽量不要使用defer。

3.4.1 - 禁止在循环中使用defer

// 错误的示例
func doSth() {
    for {
        f := open()
        defer f.Close()

        // do with f
    }
    return
}

// ------------------------
// 正确的示例
func doSth() {
    for {
        handle()
    }
    return
}

func handle() {
    f := open()
    defer f.Close()

    // do with f
}

3.5 - WaitGroup

对WaitGroup的Add操作,不要放在goroutine中执行

// 错误的示例
var wg sync.WaitGroup

for i := 0; i < 100; i++ {
    go func() {
        wg.Add(1)
        defer wg.Done()
    }()
}

wg.Wait()
// 正确的示例
var wg sync.WaitGroup

for i := 0; i < 100; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
    }()
}

wg.Wait()

4 - 错误处理

禁止使用 _ 丢弃返回的错误,所有返回的错误必须处理,保证代码健壮

4.1 - 不要忽略错误

如果一个函数的返回值包括 err,那么不要使用 _ 来忽略它,而应该去检查函数是否执行成功,如果不成功则执行对应的错误处理并返回,只有在确实不希望出现的情况下才使用 panic

4.2 - 错误消息全小写

错误消息是英文时应当全部使用小写,不要以标点符号结尾,错误例子:

func open() error {
    return errors.New("File not exist")
}

if err := open(); err != nil {
    // 打印信息会出现奇怪的大小写
    log.Printlf("Open file err: %v", err)
}

4.3 - 在逻辑处理中禁用panic

在main包中只有当实在不可运行的情况采用panic,例如文件无法打开,数据库无法连接导致程序无法 正常运行,但是对于其他的package对外的接口不能有panic。

强烈建议在main包中使用log.Fatal来记录错误,这样就可以由log来结束程序。

4.3.1 - 不要滥用 panic

不要使用 panic 来做正常的错误处理和逻辑中断处理,应当使用 error 和 多个返回值来进行。

4.4 - recover

4.4.1 - 必须在defer函数中运行

recover捕获的是祖父级调用时的panic,直接调用recover时无效

func main() {
    recover()
    panic(1)
}

直接defer调用也无效

func main() {
    defer recover()
    panic(1)
}

defer 调用时多层嵌套依然无效

func main() {
    defer func() {
        func() { recover() }()
    }()
    panic(1)
}

正确示例:

func main() {
    defer func() {
        recover()
    }()
    panic(1)
}

4.5 - err 变量

通常会把自定义的Error放在package级别中,统一进行维护,并且变量以Err开头。

var (
    ErrCacheMiss = errors.New("memcache: cache miss")
    ErrCASConflict = errors.New("memcache: compare-and-swap conflict")
)

func GetCache(key string) (string, error) {
    return "", ErrCacheMiss
}

5 - 闭包

在循环中调用函数或者goroutine方法,一定要采用显式的变量调用,不要再闭包函数里面调用循环的参数

for i:=0; i < limit; i++ {
    go func(){ DoSomething(i) }() //错误的做法
    go func(i int){ DoSomething(i) }(i)//正确的做法
}

http://golang.org/doc/articles/race_detector.html#Race_on_loop_counter


6 - struct规范

6.1 - struct 定义

使用多行定义struct,例:

type User struct {
    Name string
    Age int
}

6.2 - struct 初始化

初始化struct时禁止省略字段名

// 错误的例子
user := User {
    "hello",
    1,
}

// 正确的例子
user := User {
    Name: "hello",
    Age: 1,
}

6.3 - recieved是值类型还是指针类型

到底是采用值类型还是指针类型主要参考如下原则:

type Win struct {
    A int
}

// 值类型, 不会改变w
func(w Win) SetA(a int) {
    w.A = a
}

// 指针类型, 会改变w
func(w *Win) SetA(a int) {
    w.A = a
}

如果仍然不清楚该用值类型还是指针类型,默认用指针类型

6.4 - 带mutex/waitgroup的struct必须是指针receivers

如果你定义的struct中带有mutex/waitgroup,那么你的receivers必须是指针

6.5 - 参数传递

  • 对于少量数据,不要传递指针
  • 对于大量数据的struct可以考虑使用指针
  • 传入参数是map,slice,chan不要传递指针
  • 因为map,slice,chan是引用类型,不需要传递指针类型
  • sync.WaitGroup 作为参数传递时,需要传递指针类型

7 - 字符串规范

7.1 - 字符串为空判断

// 正确
if str == "" {
}

// 错误, 语义不正确
if len(str) == 0 {
}

7.2 - 字符串连接

禁止使用 + 做连接字符串的操作, 使用 bytes.Buffer 获得更高的性能

// 正确
var buf bytes.Buffer
buf.WriteString(a)
buf.WriteString(b)
log.Println(buf.String())

// 错误
log.Println(a+b)

7.3 - 字符串与字节流的转换

尽量减少 string 和 []byte 类型间的转换操作


8 - 包管理

默认使用go module 作为第三方依赖的包管理

8.1 - module 初始化

$go mod init ${your module name}

8.2 - 获取包

使用go get 来获取第三方包依赖

$go get github.com/go-mysql-driver/mysql

8.3 - 更新依赖包

$go get -u github.com/go-mysql-driver/mysql

9 - 单元测试

9.1 - 测试代码规范

测试代码应与被测试代码在同一package下,单元测试文件名命名规范为 example_test.go
测试用例的函数名称必须以 Test 开头,例如:TestExample, 基准测试的函数名以Bench开头,例如 BenchExample

9.2 - TDD

编写 table drive tests,易于维护测试用例

func TestSth(t *testing.T) {
    type fields struct {
        Arg string
    }
    tests := []struct {
        name   string
        fields fields
        want   string
    }{
        {name: "test a", fields: fields{Arg: "a"}, want: "a"},
        {name: "test b", fields: fields{Arg: "b"}, want: "b"},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := sth(tt.fields.Arg); got != tt.want {
                t.Errorf("sth() = %v, want %v", got, tt.want)
            }
        })
    }
}

9.3 - TestMain

使用package级别的TestMain来初始化依赖,每个package只能有一个TestMain

func TestMain(m *testing.M) {
    url := os.Getenv("DATABASE_DSN")
    if url == "" {
        return
    }

    db = open(url)

    // 运行测试用例
    exitCode := m.Run()

    db.close()

    // Exit
    os.Exit(exitCode)
}

9.4 - 运行单元测试

运行运单测试,开启go的race detection

$go test -bench . -benchmem -race -cover ./...

10 - context

context用来保存认证信息,链路信息,超时和取消控制等,go服务在rpc调用链路传递context,一般作为方法中第一个参数,命名为ctx。
不要将context作为struct的成员变量,而是在每个方法里作为参数传递

10.1 - context value

10.1.1 - 禁止使用nil作为parent context

错误示例

    ctx := context.WithValue(nil, "key", 1)
    // 这里会panic
    val := ctx.Value("null")
    log.Println(val)

正确示例

    ctx := context.WithValue(context.Background(), "key", 1)
    val := ctx.Value("null")
    log.Println(val)

10.1.2 - 使用自定义类型作为context value的key

使用自定义类型作为key防止不同包中对context赋值产生碰撞

type privateReqIDKey struct{
}

var key = privateReqIDKey{}

func SetReqId(ctx context.Context, reqID string) context.Context {
    ctx := context.WithValue(ctx, key, "abc")
    return ctx
}

func GetReqId(ctx context.Conte) string {
    reqid, _ := ctx.Value(key).(string)
    return reqid
}

11 - goroutine

11.1 - 线程安全

除了sync包中的对象,其他对象都是非线程安全的

11.2 - race condition

在goroutine中对变量进行读写操作时,需要加互斥锁/读写锁,或者使用原子操作,go build / go run 命令支持 -race 参数来检测race condition

错误示例:

var count int64
for i := 0; i < 100; i++ {
    go func() {
       count++
    }()
}
time.Sleep(time.Second)
log.Println(count)

正确示例1:

var mutex sync.Mutex
var count int64
for i := 0; i < 100; i++ {
    go func() {
        mutex.Lock()
        count++
        mutex.Unlock()
    }()
}
time.Sleep(time.Second)
log.Println(count)

正确示例2:

// 使用原子操作来获得更高的性能
var count int64
for i := 0; i < 100; i++ {
    go func() {
        atomic.AddInt64(&count, 1)
    }()
}
time.Sleep(time.Second)
log.Println(count)

12 - interface

12.1 - 接口类型转换

不允许对接口的强制类型转换
错误示例:

    var i interface{} = 1
    str := i.(string) // 这里会panic

正确示例:

    var i interface{} = 1
    str, ok := i.(string)
    log.Println(str, ok) // str is a empty string, ok is false

12.2 - nil判断

只有当interface的类型和值都为nil的时候,interface才等于nil,如果一个方法返回interface类型,要显式的返回nil
错误示例:

func GetInterface(flag bool) interface{} {
    var ret *string = nil
    if flag {
        return ret
    }
    name := "name"
    ret = &name
    return ret
}

i := GetInterface(true)

if i == nil {
    log.Println("nil")
    return
}
// 会打印这里
log.Println("not nil")

正确示例:

func GetInterface(flag bool) interface{} {
    var ret *string = nil
    if flag {
        return nil
    }
    name := "name"
    ret = &name
    return ret
}

i := GetInterface(true)

if i == nil {
    // 会打印这里
    log.Println("nil")
    return
}
log.Println("not nil")

13 - 项目结构

以SampleProj为例:

- SampleProj/  项目根目录
    |- SampleProj/  项目子目录
        |- bin/  bin目录 存放 init.script 启动脚本
            |- dev/ dev 环境
            |- uat/ uat 环境
            |- master/  线上环境
        |- configs/  存放配置文件
            |- dev/ dev 环境
            |- uat/ uat 环境
            |- master/  线上环境
        |- src/
            |- main.go main 入口
            |- */   其他代码
        |- makefile
    |- readme.MD
    |- makefile
    |- glide.yaml  glide 依赖文件
    |- glide.lock  glide 锁文件
    |- .gitignore
    |- vendor/ 第三方依赖包