GO规范-编码和命名

1991/6/26 规范

Uber 是一家美国硅谷的科技公司,也是 Go 语言的早期 adopter。其开源了很多 golang 项目,诸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年末 Uber 将内部的 Go 风格规范 开源到 GitHub,经过一年的积累和更新,该规范已经初具规模,并受到广大 Gopher 的关注,本文是对规范的整理。

https://github.com/xxjwxc/uber_go_guide_cn

# 1. 包名

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

  • 全部小写、没有大写或下划线。
  • 大多数使用命名导入的情况下,不需要重命名。
  • 简短而简洁。
  • 不用复数。例如net/url,而不是net/urls
  • 不要用common、util、shared、lib,这些不好、信息量不足的名称。

# 1.1 最后一个词不是包名

如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名。

import (
  "net/http"

  client "example.com/client-go"  
  trace "example.com/trace/v2" // 最后一个元素是v2
)
1
2
3
4
5
6

# 1.2 有冲突时

在所有其他情况下,除非导入之间有直接冲突,否则应避免导入别名。

// 不推荐写法
import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace" // 没有冲突,不使用别名
)

// 有冲突时,再起别名
import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2. 函数名

我们遵循 Go 社区关于使用 MixedCaps 作为函数名 的约定,使用驼峰命名法,不要使用下划线。举例:MixedCaps 或者mixedCaps

有一个例外,为了对相关的测试用例进行分组,函数名可能包含下划线,如:TestMyFunction_WhatIsBeingTested

# 3. 变量和常量

# 3.1 未导出带前缀

对于未导出的全局(包内)varsconsts, 前面加上前缀_,用来明确表示它们是全局符号。错误类型的变量例外,错误类型变量应以err开头。

# 1.不推荐写法

// 包内变量
const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Default port", defaultPort)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2. 推荐写法

// 包内变量
const (
  _defaultPort = 8080
  _defaultUser = "user"
)
1
2
3
4
5

# 3.2 相似声明放一组

Go 语言支持将相似的声明放在一个组内。

# 1. 反面示例

// 导入包
import "a"
import "b"
// 常量
const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64
// 不要将不相关的声明放在一组。
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2. 推荐用法

// 导入包
import (
  "a"
  "b"
)
// 常量
const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)
// 将相关的声明放在一组
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)
const EnvVar = "MY_ENV"
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

# 3.3 本地变量声明

如果将变量明确设置为某个值,则应使用短变量声明形式 (:=)。

// 反面示例
var s = "foo"
// 推荐写法
s := "foo"
1
2
3
4

但是,在某些情况下,var 使用关键字时默认值会更清晰。例如,声明空切片。

# a. 反面示例

func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}
1
2
3
4
5
6
7
8

# b. 推荐写法

func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}
1
2
3
4
5
6
7
8

# 3.4 缩小变量作用域

如果有可能,尽量缩小变量作用范围。除非它与 减少嵌套的规则冲突。

// 反面示例
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
 return err
}

// 推荐写法
if err := ioutil.WriteFile(name, data, 0644); err != nil {
 return err
}
1
2
3
4
5
6
7
8
9
10

# 4. import 分组

导入应该分为两组:标准库、其他库,用空行分开

# 4.1 反面示例

// 未用空行分割
import (
  "fmt" // 标准库 
  "os" // 标准库 
  "go.uber.org/atomic" // 其他库 
  "golang.org/x/sync/errgroup" // 其他库 
)
1
2
3
4
5
6
7

# 4.2 推荐用法

import (
  "fmt"
  "os"

  "go.uber.org/atomic" // 其他库 
  "golang.org/x/sync/errgroup" // 其他库 
)
1
2
3
4
5
6
7

# 4.defer使用

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

# 4.1 反面示例

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

# 4.2 推荐写法

p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// 更可读
1
2
3
4
5
6
7
8
9
10
11

# 5. Channel的size

Channel 的 size 要么是 1,要么是无缓冲的

Bad Good
// 应该足以满足任何情况! c := make(chan int, 64) // 大小:1 c := make(chan int, 1) // 或者无缓冲 channel,大小为 0 c := make(chan int)

# 6. 枚举从1开始

Go 中引入枚举的标准方法是声明一个自定义类型和一个使用了 iotaconst 组。由于变量的默认值为 0,因此通常应以非零值开头枚举。

# 6.1 反面示例

type Operation int

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

# 6.2 推荐写法

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

// Add=1, Subtract=2, Multiply=3
1
2
3
4
5
6
7
8
9

# 7. 断言使用

类型断言 将会在检测到不正确的类型时,以单一返回值形式时会抛 panic

// 反面示例
t := i.(string)

// 推荐写法
t, ok := i.(string)
if !ok {
  // 优雅地处理错误
}
1
2
3
4
5
6
7
8

# 8. 不要使用panic

在生产环境中运行的代码必须避免出现 panicpanic 是 级联失败 的主要根源 。如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它。

# 8.1 反面示例

func run(args []string) {
  if len(args) == 0 {
    panic("an argument is required")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}
1
2
3
4
5
6
7
8
9
10

# 8.2 推荐写法

func run(args []string) error {
  if len(args) == 0 {
    return errors.New("an argument is required")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 9. 避免使用init()

尽可能避免使用init()。当init()是不可避免或可取的,代码应先尝试:

  1. 无论程序环境或调用如何,都要完全确定。
  2. 避免依赖于其他init()函数的顺序或副作用。虽然init()顺序是明确的,但代码可以更改, 因此init()函数之间的关系可能会使代码变得脆弱和容易出错。
  3. 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录、程序参数/输入等。
  4. 避免I/O,包括文件系统、网络和系统调用。

尽可能的作为main()函数流程中调用的一个环节。

# 9.1 反面示例

type Config struct {
    // ...
}
var _config Config
func init() {
    // Bad: 基于当前目录
    cwd, _ := os.Getwd()
    // Bad: I/O
    raw, _ := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    yaml.Unmarshal(raw, &_config)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 9.2 推荐写法

type Config struct {
    // ...
}
// 提供一个获取配置的方法
func loadConfig() Config {
    cwd, err := os.Getwd()
    // handle err
    raw, err := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // handle err
    var config Config
    yaml.Unmarshal(raw, &config)
    return config
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10. 流程优化

# 10.1 减少嵌套

代码应尽可能先处理错误情况、特殊情况并尽早返回,依次来减少嵌套。

# a. 反面示例

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("Invalid v: %v", v)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# b. 推荐写法

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()
}
1
2
3
4
5
6
7
8
9
10
11
12

# 10.2 不必要的else

// 反面示例
var a int
if b {
  a = 100
} else {
  a = 10
}

// 推荐写法
a := 10
if b {
  a = 100
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 11. 字符串

# 11.1 使用**``**,避免转义

// 反面示例
wantError := "unknown name:\"test\""

//推荐写法
wantError := `unknown error:"test"`
1
2
3
4
5

# 11.2 格式化字符串

如果你在函数外声明Printf-style 函数的格式字符串,请将其设置为const常量。

这有助于go vet对格式字符串执行静态分析。

// 反面示例
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

// 推荐写法
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
1
2
3
4
5
6
7