简介

本指南概述了在 Uber 编写 Go 代码的约定和最佳实践。目标是通过提供清晰的指南来管理代码复杂性,确保代码库的可维护性,同时让工程师能够有效利用 Go 的特性。

所有代码都应通过 golintgo vet 检查。建议在保存时运行 goimports,并使用 golintgo vet 检查错误。

指南

指向接口的指针

几乎不需要使用指向接口的指针。即使底层数据是指针,接口也应作为值传递。

验证接口合规性

在适当的地方编译时验证接口合规性,以确保类型实现了所需的接口。

type Handler struct {
  // ...
}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // ...
}

接收器和接口

带有值接收器的方法可以在值和指针上调用,而带有指针接收器的方法只能在指针或可寻址的值上调用。

零值 Mutex 是有效的

sync.Mutexsync.RWMutex 的零值是有效的,因此很少需要指向 mutex 的指针。

var mu sync.Mutex
mu.Lock()

在边界处复制切片和映射

切片和映射包含指向底层数据的指针,因此在复制时要小心,以避免意外的副作用。

使用 defer 清理资源

使用 defer 清理文件、锁等资源,确保即使发生错误,资源也能正确释放。

p.Lock()
defer p.Unlock()

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

p.count++
return p.count

通道大小为一或无

通道的大小通常应为一或无缓冲。除非绝对必要,否则避免使用大缓冲区。

c := make(chan int, 1) // 或者
c := make(chan int)

枚举从 1 开始

枚举从 1 开始,以避免零值成为有效但非预期的状态。

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

使用 "time" 处理时间

始终使用 time 包处理时间,以避免与时间计算相关的常见问题。

错误处理

错误类型

对于静态错误消息,使用 errors.New;对于动态错误消息,使用 fmt.Errorf。对于需要匹配的错误,使用自定义错误类型。

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
  return ErrCouldNotOpen
}

错误包装

使用 fmt.Errorf%w 动词包装错误以提供上下文。

if err != nil {
  return fmt.Errorf("new store: %w", err)
}

错误命名

根据错误是否导出,使用 Errerr 作为错误值的前缀。

var (
  ErrBrokenLink = errors.New("link is broken")
  errNotFound   = errors.New("not found")
)

只处理一次错误

只处理一次错误。避免记录错误后再返回它。

if err := emitMetrics(); err != nil {
  log.Printf("Could not emit metrics: %v", err)
}

处理类型断言失败

执行类型断言时,始终使用 "comma ok" 惯用法以避免 panic。

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

不要 panic

在生产代码中避免使用 panic。相反,返回错误并让调用者决定如何处理。

使用 go.uber.org/atomic

使用 go.uber.org/atomic 进行原子操作,以避免 sync/atomic 包中的常见错误。

type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
    return
  }
  // 启动 Foo
}

避免可变全局变量

避免修改全局变量。使用依赖注入代替。

避免在公共结构体中嵌入类型

避免在公共结构体中嵌入类型,以防止泄露实现细节。

避免使用内置名称

避免使用 Go 的预声明标识符作为变量名,以防止遮蔽和混淆。

避免使用 init()

尽可能避免使用 init()。如果必须使用,请确保它是确定性的,并且不依赖于外部状态。

main 中退出

仅在 main() 中调用 os.Exitlog.Fatal。所有其他函数应返回错误。

func main() {
  if err := run(); err != nil {
    log.Fatal(err)
  }
}

func run() error {
  // ...
}

在序列化结构体中使用字段标签

在序列化为 JSON、YAML 或其他格式的结构体中使用字段标签。

type Stock struct {
  Price int    `json:"price"`
  Name  string `json:"name"`
}

不要启动后不管的 Goroutine

确保 Goroutine 有明确的退出点,并正确清理。

var (
  stop = make(chan struct{})
  done = make(chan struct{})
)

go func() {
  defer close(done)
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

close(stop)
<-done

性能

优先使用 strconv 而不是 fmt

将基本类型转换为字符串时,使用 strconv 而不是 fmt,以获得更好的性能。

避免重复的字符串到字节转换

避免重复将相同的字符串转换为字节切片。转换一次并重用结果。

优先指定容器容量

尽可能指定切片和映射的容量,以避免不必要的分配。

data := make([]int, 0, size)

风格

避免过长的行

避免需要水平滚动的代码行。目标是软限制为 99 个字符。

保持一致性

一致性是关键。在整个代码库中遵循相同的风格。

分组相似的声明

将相似的声明分组以提高可读性。

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

导入分组顺序

将导入分为标准库和第三方库。

import (
  "fmt"
  "os"

  "go.uber.org/atomic"
)

包名

选择简短、描述性的包名,全部小写且不为复数。

函数名

使用 MixedCaps 命名函数。测试函数可以包含下划线以进行分组。

导入别名

仅在必要时使用导入别名以解决命名冲突。

函数分组和排序

按接收器分组函数,并按调用顺序排序。

减少嵌套

通过提前处理错误情况和特殊情况来减少嵌套。

不必要的 else

当变量可以在单个 if 语句中设置时,避免不必要的 else 块。

顶层变量声明

除非类型不明显,否则使用 var 进行顶层变量声明。

未导出的全局变量前缀为 _

为避免意外使用,未导出的顶层变量和常量应前缀为 _

结构体中的嵌入

仅在提供实际好处时才在结构体中嵌入类型。避免嵌入互斥锁。

局部变量声明

尽可能使用短变量声明 (:=) 声明局部变量。

nil 是有效的切片

使用 nil 表示空切片,而不是显式返回空切片。

减少变量作用域

尽可能减少变量的作用域以提高可读性。

避免裸参数

避免在函数调用中使用裸参数。使用注释或命名类型以提高清晰度。

使用原始字符串字面量避免转义

使用原始字符串字面量以避免字符串中的转义字符。

初始化结构体

使用字段名初始化结构体

初始化结构体时始终使用字段名。

k := User{
  FirstName: "John",
  LastName:  "Doe",
}

省略结构体中的零值字段

初始化结构体时省略零值字段。

user := User{
  FirstName: "John",
  LastName:  "Doe",
}

使用 var 声明零值结构体

使用 var 声明零值结构体。

var user User

初始化结构体引用

初始化结构体引用时使用 &T{} 而不是 new(T)

sptr := &T{Name: "bar"}

初始化映射

使用 make 初始化空映射,使用映射字面量初始化具有固定元素的映射。

m := make(map[T1]T2, size)

Printf 外部声明格式字符串

Printf 风格的函数外部声明格式字符串为 const 值。

命名 Printf 风格的函数

命名 Printf 风格的函数时使用 f 后缀以启用 go vet 检查。

模式

测试表

使用带有子测试的表驱动测试来避免重复代码。

tests := []struct {
  give     string
  wantHost string
  wantPort string
}{
  // ...
}

for _, tt := range tests {
  t.Run(tt.give, func(t *testing.T) {
    host, port, err := net.SplitHostPort(tt.give)
    require.NoError(t, err)
    assert.Equal(t, tt.wantHost, host)
    assert.Equal(t, tt.wantPort, port)
  })
}

函数式选项

在构造函数和公共 API 中使用函数式选项来处理可选参数。

type Option interface {
  apply(*options)
}

func WithCache(c bool) Option {
  return cacheOption(c)
}

func Open(addr string, opts ...Option) (*Connection, error) {
  // ...
}

代码检查

在整个代码库中使用一致的代码检查工具。推荐的代码检查工具包括:

代码检查运行器

使用 golangci-lint 作为 Go 代码的代码检查运行器。它支持许多代码检查工具,并可以通过 .golangci.yml 文件进行配置。

linters:
  enable:
    - errcheck
    - goimports
    - golint
    - govet
    - staticcheck

本指南提供了在 Uber 编写 Go 代码的全面最佳实践。通过遵循这些指南,您可以确保代码的可维护性、高效性和符合 Go 的习惯用法。

FunTester 原创精华

【连载】从 Java 开始性能测试


↙↙↙阅读原文可查看相关链接,并与作者交流