本指南概述了在 Uber 编写 Go 代码的约定和最佳实践。目标是通过提供清晰的指南来管理代码复杂性,确保代码库的可维护性,同时让工程师能够有效利用 Go 的特性。
所有代码都应通过 golint
和 go vet
检查。建议在保存时运行 goimports
,并使用 golint
和 go vet
检查错误。
几乎不需要使用指向接口的指针。即使底层数据是指针,接口也应作为值传递。
在适当的地方编译时验证接口合规性,以确保类型实现了所需的接口。
type Handler struct {
// ...
}
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// ...
}
带有值接收器的方法可以在值和指针上调用,而带有指针接收器的方法只能在指针或可寻址的值上调用。
sync.Mutex
和 sync.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 开始,以避免零值成为有效但非预期的状态。
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)
}
根据错误是否导出,使用 Err
或 err
作为错误值的前缀。
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
。相反,返回错误并让调用者决定如何处理。
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.Exit
或 log.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 有明确的退出点,并正确清理。
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 命名函数。测试函数可以包含下划线以进行分组。
仅在必要时使用导入别名以解决命名冲突。
按接收器分组函数,并按调用顺序排序。
通过提前处理错误情况和特殊情况来减少嵌套。
当变量可以在单个 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) {
// ...
}
在整个代码库中使用一致的代码检查工具。推荐的代码检查工具包括:
errcheck
goimports
golint
govet
staticcheck
使用 golangci-lint
作为 Go 代码的代码检查运行器。它支持许多代码检查工具,并可以通过 .golangci.yml
文件进行配置。
linters:
enable:
- errcheck
- goimports
- golint
- govet
- staticcheck
本指南提供了在 Uber 编写 Go 代码的全面最佳实践。通过遵循这些指南,您可以确保代码的可维护性、高效性和符合 Go 的习惯用法。
FunTester 原创精华