Go Code Style Guide

命名

包名

当命名包时,请遵守下面规则:

  • 全部小写。没有大写或下划线。
  • 简短而简洁。请记住,在每个使用的地方都完整标识了该名称。
  • 不建议使用“common”,“util”,“shared”或“lib”。这些是信息量不足的名称。

BAD

1
2
kv_log
rateLimit

GOOD

1
2
kvlog
ratelimit

函数、变量名

遵循 Go 社区关于使用 MixedCaps 作为函数名 的约定。使用单词首字母大小写命名,作用域越大的名称越详细,名字不包含下划线。
有一个例外,为了对相关的测试用例进行分组,函数名可能包含下划线,如:TestMyFunction_WhatIsBeingTested.
函数名字使用动词加名词的形式,避免使用v1/v2这类后缀.

BAD

1
2
var global_variable bool
const MAX_SIZE = 10

GOOD

1
2
var globalVariable bool
const MaxSize = 10

时间相关变量命名带上单位。

BAD

1
var expireTime int

GOOD

1
var expireMilliSeconds int

接收者命名

使用一致的名字.
不推荐this、self,原因是跟开源社区习惯不一致。
BAD

1
2
3
type foo struct {}
func (f foo) A() {}
func (pf *foo) B() {} // 这类的pf跟前面使用的f不一致

GOOD

1
2
3
type foo struct {}
func (f foo) A() {}
func (f *foo) B() {}

函数和接口定义

函数长度

不超过100行。

函数复杂度

圈复杂度小于12(易于测试)
认知复杂度小于15(便于理解)

参数和返回值个数

参数少于5个,返回值少于3个.
过多的参数请根据它们的用途归类到参数对象传递.

context参数

必须作为函数第一个参数出现
禁止业务代码通过context传递参数,以明确的参数代替。
BAD

1
2
3
4
5
6
7
8
9
func foo(ctx context.Context) {
isOldUser, ok := ctx.Value("is_old_user").(boo) // ctx禁止传递业务参数
if !ok {
return
}
if isOldUser {
...
}
}

GOOD

1
2
3
4
5
func foo(ctx context.Context, isOldUser bool) {
if isOldUser {
...
}
}

语句

枚举从1开始

0是Go语言的默认值,通常只有在它能代表一定意义才会使用,否则应该避免0值。
BAD

1
2
3
4
5
6
7
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2

GOOD

1
2
3
4
5
6
7
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3

使用字段名初始化结构体

初始化结构体时,应该指定字段名称。
BAD

1
k := User{"John", "Doe", true}

GOOD

1
2
3
4
5
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}

用常量代替魔法数字

对于有意义的魔法数字,必须用常量代替

BAD

1
2
3
4
5
6
switch processState {
case 1:
...
case 2:
...
}

GOOD

1
2
3
4
5
6
switch processState {
case stateSuccessfully:
...
case stateExitWithError:
...
}

使用 defer 释放资

使用 defer 释放资源,诸如文件和锁。

BAD

1
2
3
4
5
6
7
8
9
10
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// 当有多个 return 分支时,很容易遗忘 unlock

GOOD

1
2
3
4
5
6
7
8
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// 更可读

返回错误

错误类型必须是error接口
如果调用者需要检测某种特定错误,必须返回一个特定哨兵错误。
如果自定义错误类型,最好提供检查错误的方法。

BAD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// package foo
func Open() error {
return errors.New("could not open")
}
// package bar
func use() {
if err := foo.Open(); err != nil {
if err.Error() == "could not open" {
// handle
} else {
panic("unknown error")
}
}
}

GOOD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
if err == foo.ErrCouldNotOpen { // 哨兵错误值
// handle
} else {
panic("unknown error")
}
}
type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func open(file string) error {
return errNotFound{file: file}
}

func IsNotFoundError(err error) bool {
_, ok := err.(errNotFound)
return ok
}

func use() {
if err := open("testfile.txt"); err != nil {
if _, ok := IsNotFoundError(err); ok {
// handle
} else {
panic("unknown error")
}
}
}

处理函数返回的error

最好处理所有error。
对于那些不关心的error使用下划线占位符。

BAD

1
2
func foo() error { return errors.New("error") }
foo() // 未处理的错误

GOOD

1
2
3
4
func foo() error { return errors.New("error") }
if err := foo(); err != nil {
...
}

处理类型断言失败

使用”comma ok”的语句形式

BAD

1
t := i.(string)

GOOD

1
2
3
4
t, ok := i.(string)
if !ok {
// 优雅地处理错误
}

处理panic

对于可以预料的错误不应该产生panic,应该返回错误由调用者处理,必要时加上监控或上报sentry。

BAD

1
2
3
4
5
func raiseError(e string) {
if e == "" {
panic(errors.New("error occur"))
}
}

GOOD

1
2
3
4
5
6
func raiseError(e string) error {
if e == "" {
return errors.New("error occur")
}
return nil
}

管理goruntine

goruntine需要在入口处recover住panic并处理panic。
goruntine要有明确的生命周期管理,如果有长时间运行的goruntine,需要通过context传递结束信号。

BAD

1
2
3
4
5
6
7
8
func mayPanic() {
if rand.Int()%2 == 1 {
panic(errors.New("surprise"))
}
}
go func() {
mayPanic()
} ()

GOOD

1
2
3
4
5
6
7
8
go func() {
defer func() {
if e := recover(); e != nil {
log.Print(e)
}
}()
mayPanic()
}()

切片 Slice

  • 取元素之前需要确保索引不超过有效长度。

  • 要检查切片是否为空,请始终使用len(s) == 0。而非 nil。

    1
    2
    3
    func isEmpty(s []string) bool {
    return len(s) == 0
    }
  • 不应明确返回长度为零的切片。应该返回nil来代替

    1
    2
    3
    if x == "" {
    return nil
    }
  • 零值切片(用var声明的切片)可立即使用,无需调用make()创建。

    1
    2
    3
    4
    5
    6
    7
    var nums []int
    if add1 {
    nums = append(nums, 1)
    }
    if add2 {
    nums = append(nums, 2)
    }

在边界处拷贝 Slices 和 Maps

slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。
接收 Slices 和 Maps
请记住,当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。

BAD

1
2
3
4
5
6
7
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// 你是要修改 d1.trips 吗?
trips[0] = ...

GOOD

1
2
3
4
5
6
7
8
9
10
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
trips := ...
d1.SetTrips(trips)
// 这里我们修改 trips[0],但不会影响到 d1.trips
trips[0] = ...
返回 slices 或 maps
同样,请注意用户对暴露内部状态的 map 或 slice 的修改。

BAD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Stats struct {
mu sync.Mutex
counters map[string]int
}
// Snapshot 返回当前状态。
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
return s.counters
}
// snapshot 不再受互斥锁保护
// 因此对 snapshot 的任何访问都将受到数据竞争的影响
// 影响 stats.counters
snapshot := stats.Snapshot()

GOOD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Stats struct {
mu sync.Mutex
counters map[string]int
}
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}
// snapshot 现在是一个拷贝
snapshot := stats.Snapshot()

减少嵌套

代码应通过尽可能先处理错误情况/特殊情况并尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码的代码量。
嵌套层级最好不要超过3层。

BAD

1
2
3
4
5
6
7
8
9
10
11
12
for _, v := range data { // 第一层
if v.F1 == 1 { // 第二层
v = process(v)
if err := v.Call(); err == nil { // 第三层
v.Send()
} else {
return err
}
} else {
log.Printf("quot;Invalid v: %v", v)
}
}

GOOD

1
2
3
4
5
6
7
8
9
10
11
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
}

避免在闭包中引用循环变量

在闭包中引用循环变量经常会导致非预期的结果,产生bug。

BAD

1
2
3
4
5
6
7
8
9
10
11
for _, s := range []string{"a", "b"} {
go func() {
fmt.Println(s)
}()
}
GOOD
for _, s := range []string{"a", "b"} {
go func(v string) {
fmt.Println(v)
}(s) // 传值
}

GOOD

1
2
3
4
5
6
7
8
9
10
11
12
13
type option struct {
A int
}
type config struct {
opt *option
}
func newConfig(opt *option) *config {
return &config{opt: opt}
}
for _, opt := range []option{option{A:10}, option{A:20}} {
v := opt // 拷贝一次
newConfig(&v) // 把临时变量指针保存以后使用
}

不必要的 else

如果在 if 的两个分支中都设置了变量,则可以将其替换为单个 if。

BAD

1
2
3
4
5
6
var a int
if b {
a = 100
} else {
a = 10
}

GOOD

1
2
3
4
a := 10
if b {
a = 100
}

外部调用

设置超时

调用外部系统例如HTTP服务、数据库等必须设置一个超时时间

BAD

1
http.Get(url)

GOOD

1
2
3
client := &http.Client{
Timeout: time.Millisecond * time.Duration(conf.TimeoutMS),
}

避免数据库事务被用户取消

在事务中谨慎使用从用户请求传递过来的context,因为可能会随着用户请求取消而产生非预期的行为

BAD

1
2
3
func requestHandle(ctx context.Context, param interface{}) {
db.Txx(ctx, param() // 直接调用会因为用户取消而导致事务失败,如果这个行为不是你想要的,那么很可能会导致问题
}

GOOD

1
2
3
4
5
func requestHandle(ctx context.Context, param interface{}) {
txCtx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
db.Txx(txCtx, param() // 重新设定超时时间,用户取消也会在超时时间内执行。
}

避免散播存储层的错误

避免调用者检查数据库返回的错误,需要把关注的错误码转换成其他形式返回给调用者

BAD

1
2
3
4
5
6
7
8
9
10
func getValueFromRedis(key string) (val *struct{}, err error) {
return nil, redis.Nil
}
func foo() {
v, err := getValueFromRedis("")
// 在下层处理redis.Nil,返回(nil,nil)或者自定义错误(nil, ErrKeyNotExists)
if err == redis.Nil {
...
}
}

GOOD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func getValueFromRedis(key string) (val *struct{}, err error) {
if err == redis.Nil {
return nil, nil
}
...
return
}
func foo() {
v, err := getValueFromRedis("")
if err != nil {
return
}
// 判断是否为空指针
if v == nil {
return
}
}

文件布局

从上到下依次为:

  • 常量定义
  • 变量定义
  • 类型定义
  • 类型构造函数
  • 类型方法
  • (多个类型)…
  • 全局函数

函数根据调用顺序,被调用函数放在调用者之后
类型定义和类型构造函数、类型方法定义不能分散到多个文件里面

测试

使用表驱动测试

使用表驱动搭配断言来编写测试用例

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2015-2024 RivenZoo
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信