引言
样式是支配我们代码的惯例。 术语“样式”有点用词不当,因为这些约定不仅仅涵盖那些可以由gofmt替我们处理的源文件格式。
本指南的目的是通过详细描述在Uber编写Go代码的注意事项来管理这种复杂性。 这些规则的存在是为了使代码库易于管理,同时仍然允许工程师有效地使用Go语言功能。
该指南最初由Prashant Varanasi和Simon Newton编写,目的是使一些同事快速使用Go。 多年来,已根据其他人的反馈进行了修改。
本文档记录了我们在Uber遵循的Go代码中的惯用约定。 其中许多是Go的通用准则,而其他准则则依赖于外部资源:
通过golint
和go vet
运行时,所有代码均应无错误。 我们建议您将编辑器设置为:
- 在保存时运行
goimports
- 运行
golint
和go vet
以检查错误
您可以在以下位置的Go工具的编辑器支持中找到信息:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
准则
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.Mutex
和sync.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.Fatal
或t.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。
- 不用
common
,util
,shared
或lib
。这些是不好的,无用的名称。
可以参考 Package Names和 style guideline for Go packages.
函数命名
导入别名
函数分组排序
- 函数应按粗略的调用顺序排序。
- 文件中的函数应按接收者分组。
因此,导出的函数应首先出现在文件中,然后是struct
,const
,var
定义。
减少嵌套
代码应通过尽可能先处理错误情况/特殊情况并尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码量。
不必要的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)
,以使其与结构初始化一致。