Uber 公司Golang编程规范【翻译】

引言

样式是支配我们代码的惯例。 术语“样式”有点用词不当,因为这些约定不仅仅涵盖那些可以由gofmt替我们处理的源文件格式。

本指南的目的是通过详细描述在Uber编写Go代码的注意事项来管理这种复杂性。 这些规则的存在是为了使代码库易于管理,同时仍然允许工程师有效地使用Go语言功能。

该指南最初由Prashant Varanasi和Simon Newton编写,目的是使一些同事快速使用Go。 多年来,已根据其他人的反馈进行了修改。

本文档记录了我们在Uber遵循的Go代码中的惯用约定。 其中许多是Go的通用准则,而其他准则则依赖于外部资源:

  1. Effective Go
  2. The Go common mistakes guide

通过golintgo vet运行时,所有代码均应无错误。 我们建议您将编辑器设置为:

准则

Pointers to Interfaces

您几乎不需要指向接口的指针。 您应该将接口作为值传递-底层数据仍然可以是指针。

一个接口是两个字段:

  • 指向某些特定类型信息的指针。 您可以将其视为“类型”。
  • 数据指针。 如果存储的数据是指针,则直接存储。 如果存储的数据是一个值,则存储指向该值的指针。

如果要接口方法修改基础数据,则必须使用指针。

接收器和接口

具有值接收器的方法可以在指针和值上调用。
例如

type S struct {
  data string
}

func (s S) Read() string {
  return s.data
}

func (s *S) Write(str string) {
  s.data = str
}

sVals := map[int]S{1: {"A"}}

// You can only call Read using a value
sVals[1].Read()

// This will not compile:
//  sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// You can call both Read and Write using a pointer
sPtrs[1].Read()
sPtrs[1].Write("test")

同样,即使该方法具有值接收器,也可以通过指针来满足接口。

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}

var i F
i = s1Val
i = s1Ptr
i = s2Ptr

// The following doesn't compile, since s2Val is a value, and there is no value receiver for f.
//   i = s2Val

Effective Go has a good write up on Pointers vs. Values.

零值互斥是有效的

sync.Mutexsync.RWMutex的零值是有效的,因此不需要指向互斥量的指针。

  • 推荐使用:
var mu sync.Mutex
mu.Lock()
  • 不推荐使用:
mu := new(sync.Mutex)
mu.Lock()

如果通过指针使用结构,则互斥体可以是非指针字段,或者最好直接嵌入到该结构中。

  • 为专用类型或需要实现Mutex接口的类型嵌入。
type smap struct {
  sync.Mutex

  data map[string]string
}

func newSMap() *smap {
  return &smap{
    data: make(map[string]string),
  }
}

func (m *smap) Get(k string) string {
  m.Lock()
  defer m.Unlock()

  return m.data[k]
}
  • 对于导出的类型,使用专用锁。
type SMap struct {
  mu sync.Mutex

  data map[string]string
}

func NewSMap() *SMap {
  return &SMap{
    data: make(map[string]string),
  }
}

func (m *SMap) Get(k string) string {
  m.mu.Lock()
  defer m.mu.Unlock()

  return m.data[k]
}

在边界处复制Slices和Maps

Slices和Maps包含指向基础数据的指针,因此在需要复制它们时要特别注意方案。

作为参数接收Slices和Maps

  • 推荐
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// We can now modify trips[0] without affecting d1.trips.
trips[0] = ...
  • 不推荐
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// Did you mean to modify d1.trips?
trips[0] = ...

返回时Slices 和 Maps

同样,请注意用户对显示内部状态的map或切片的修改。

  • 推荐
type Stats struct {
  sync.Mutex

  counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
  s.Lock()
  defer s.Unlock()

  result := make(map[string]int, len(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}

// Snapshot is now a copy.
snapshot := stats.Snapshot()
  • 不推荐
type Stats struct {
  sync.Mutex

  counters map[string]int
}

// Snapshot returns the current stats.
func (s *Stats) Snapshot() map[string]int {
  s.Lock()
  defer s.Unlock()

  return s.counters
}

// snapshot is no longer protected by the lock!
snapshot := stats.Snapshot()

用defer处理清理工作

使用defer清理资源,例如文件和锁。

  • 推荐
p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// more readable
  • 不推荐
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// easy to miss unlocks due to multiple returns

Defer的开销非常小,只有在您可以证明函数执行时间处于纳秒级的程度时,才应避免这样做。 比起微不足道的成本,使用defer的可读性是值得的。 对于具有比简单的内存访问更多的更大的方法尤其如此,其他方法的计算比defer要重要得多。

Channel的通道大小为1 或 无缓冲

通道通常应为1大小或无缓冲。 默认情况下,通道是无缓冲的,大小为零。 任何其他大小都必须经过严格的审查。 考虑如何确定大小,什么阻塞通道在负载下填满并阻塞写入器,以及发生这种情况时会发生什么。

  • 推荐
// Size of one
c := make(chan int, 1) // or
// Unbuffered channel, size of zero
c := make(chan int)
  • 不推荐
// Ought to be enough for anybody!
c := make(chan int, 64)

从1 开始枚举

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

  • 推荐
type Operation int

const (
 Add Operation = iota + 1
 Subtract
 Multiply
)

// Add=1, Subtract=2, Multiply=3
  • 不推荐
type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

// Add=0, Subtract=1, Multiply=2

在某些情况下,使用零值是有意义的,例如,当零值是理想的默认行为时。

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

错误类型

有多种声明错误的选项:

  • errors.New对于简单静态字符串的错误
  • fmt.Errorf用于格式化的错误字符串
  • 实现Error()方法的自定义类型
  • 包装错误使用"pkg/errors".Wrap
    返回错误时,请考虑以下因素以确定最佳选择:
  • 这是一个不需要额外信息的简单错误吗?如果是,errors.New 可以满足
  • 客户需要检测并处理此错误吗? 如果是这样,则应使用自定义类型,并实现Error()方法。
  • 您是否正在传播下游函数返回的错误?如果是,查看section on error wrapping.
  • 否则, fmt.Errorf就ok。

如果客户端需要检测错误,并且您已经使用errors创建了一个简单的错误,请使用var。

  • 推荐
// 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")
  }
}
  • 不推荐
// 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")
    }
  }
}

如果您有可能需要客户端检测的错误,并且想向其添加更多信息(例如,它不是静态字符串),则应该使用自定义类型。

  • 推荐
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 use() {
  if err := open(); err != nil {
    if _, ok := err.(errNotFound); ok {
      // handle
    } else {
      panic("unknown error")
    }
  }
}
  • 不推荐
func open(file string) error {
  return fmt.Errorf("file %q not found", file)
}

func use() {
  if err := open(); err != nil {
    if strings.Contains(err.Error(), "not found") {
      // handle
    } else {
      panic("unknown error")
    }
  }
}

直接导出自定义错误类型时要小心,因为它们已成为程序包公共API的一部分。 最好公开匹配器功能以检查错误。

// package foo

type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

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

func Open(file string) error {
  return errNotFound{file: file}
}

// package bar

if err := foo.Open("foo"); err != nil {
  if foo.IsNotFoundError(err) {
    // handle
  } else {
    panic("unknown error")
  }
}

错误包装

调用失败时,有三种主要的错误传播方式:

  • 如果没有要添加的其他上下文,并且您想要维护原始错误类型,则返回原始错误。
  • 使用pkg / errors增加上下文。包装以便错误消息提供更多上下而且pkg / errors.Cause可用于提取原始错误。
  • 如果调用方不需要检测或处理该特定错误情况使用fmt.Errorf
    使用示例:
  • 推荐
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %s", err)
}
//x: y: new store: the error
  • 不推荐
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %s", err)
}
//failed to x: failed to y: failed to create new store: the error

但是,一旦将错误发送到另一个系统,就应该清楚该消息是一个错误(例如,日志中的err标记或“Failed”前缀)。

处理类型断言失败处理类型断言失败

类型断言的单个返回值形式对不正确的类型将会panic。 因此,请始终使用“,ok”的习惯用法。

  • 推荐使用
t, ok := i.(string)
if !ok {
  // handle the error gracefully
}
  • 不推荐单个返回值:
t := i.(string)

不要使用panic

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

  • 推荐
func foo(bar string) error {
  if len(bar) == 0
    return errors.New("bar must not be empty")
  }
  // ...
  return nil
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  if err := foo(os.Args[1]); err != nil {
    panic(err)
  }
}
  • 不推荐
func foo(bar string) {
  if len(bar) == 0 {
    panic("bar must not be empty")
  }
  // ...
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  foo(os.Args[1])
}

Panic/recover不是错误处理策略。仅当发生不可恢复的事情(例如取消nil引用)时,程序才必须panic。程序初始化是一个例外:程序启动时应使程序中止的不良情况可能会引起panic

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

即使在测试中,也要优先选择t.Fatalt.FailNow来代替panic,以确保测试标记为失败。

  • 推荐
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  t.Fatal("failed to set up test")
}
  • 不推荐
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  panic("failed to set up test")
}

go.uber.org/atomic

使用sync / atomic包的原子操作对原始类型(int32,int64等)进行操作,因此很容易忘记使用原子操作来读取或修改变量。

go.uber.org/atomic通过隐藏基础类型为这些操作增加了类型安全性。 另外,它包括一个方便的atomic.Bool类型。

  • 推荐
type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // already running…
     return
  }
  // start the Foo
}

func (f *foo) isRunning() bool {
  return f.running.Load()
}
  • 不推荐官方:
type foo struct {
  running int32  // atomic
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // already running…
     return
  }
  // start the Foo
}

func (f *foo) isRunning() bool {
  return f.running == 1  // race!
}

性能

优先使用strconv而不是fmt

  • 推荐
# BenchmarkStrconv-4    64.2 ns/op    1 allocs/op
for i := 0; i < b.N; i++ {
  s := strconv.Itoa(rand.Int())
}
  • 不推荐
# BenchmarkFmtSprint-4    143 ns/op    2 allocs/op
for i := 0; i < b.N; i++ {
  s := fmt.Sprint(rand.Int())
}

避免string到[]byte的转换

不要重复从固定字符串创建字节片。相反,请执行一次转换并捕获结果。

  • 推荐
# BenchmarkGood-4  500000000   3.25 ns/op
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}
  • 不推荐
# BenchmarkBad-4   50000000   22.2 ns/op
for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}

风格

组相似声明

  • 推荐
import (
  "a"
  "b"
)

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)
  • 不推荐
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
)

const ENV_VAR = "MY_ENV"
  • 不推荐
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  ENV_VAR = "MY_ENV"
)

组不受使用位置的限制。
例如,您可以在函数内部使用它们。

func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  ...
}

import 顺序

应该有两个导入组:

  • 标准库
  • 其他一切
    默认情况下,这是goimports应用的分组。
import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

包命名

命名包时,按如下原则选择:

  • 全部小写。没有大写或下划线
  • 大多数情况不需要在导入时重命名
  • 简短而简洁。请记住,在每个调用站点上都完整标识了该名称。
  • 不复数。例如,net / url,而不是net / url。
  • 不用commonutilsharedlib。这些是不好的,无用的名称。

可以参考 Package Namesstyle guideline for Go packages.

函数命名

导入别名

函数分组排序

  • 函数应按粗略的调用顺序排序。
  • 文件中的函数应按接收者分组。
    因此,导出的函数应首先出现在文件中,然后是structconstvar定义。

减少嵌套

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

不必要的else

顶级变量声明

在顶层,使用标准的var关键字。请勿指定类型,除非它与表达式的类型不同。

使用_前缀未导出的全局变量

嵌入结构

type Client struct {
  http.Client

  version int
}

使用字段名称初始化结构

## suggest
k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}
## no suggest
k := User{"John", "Doe", true}
  • 例外: 如果字段数少于或等于3,则测试表中的字段名可以省略。

局部变量声明

如果将变量显式设置为某个值,则应使用短变量声明:=

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

nil是有效切片

nil是长度为0的有效切片。

缩小变量范围

# suggest
if err := ioutil.WriteFile(name, data, 0644); err != nil {
 return err
}

# no suggest
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
 return err
}

初始化结构引用

初始化结构引用时,请使用&T{}而不是new(T),以使其与结构初始化一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值