写点什么

Uber Go 编程风格指南

作者:FunTester
  • 2025-02-08
    河北
  • 本文字数:3257 字

    阅读完需:约 11 分钟

简介

本指南概述了在 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.Mutexmu.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) { // ...}
复制代码

代码检查

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


  • errcheck

  • goimports

  • golint

  • govet

  • staticcheck

代码检查运行器

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


linters:  enable:    - errcheck    - goimports    - golint    - govet    - staticcheck
复制代码


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

发布于: 刚刚阅读数: 5
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
Uber Go 编程风格指南_FunTester_InfoQ写作社区