• 原文地址:https://github.com/uber-go/guide/blob/master/style.md

  • 译文出处:https://github.com/uber-go/guide

  • 本文永久链接:

  • https://github.com/gocn/translator/blob/master/2019/w38ubergostyleguide.md

  • 译者:咔叽咔叽

  • 校对者:fivezh,cvley

目录

  • 介绍

  • 指南

  • 接口的指针

  • 接收者和接口

  • 零值 Mutexes 是有效的

  • 复制 Slice 和 Map

  • Defer 的使用

  • channel 的大小是 1 或者 None

  • 枚举值从 1 开始

  • Error 类型

  • Error 包装

  • 处理类型断言失败

  • 避免 Panic

  • 使用 go.uber.org/atomic

  • 性能

  • strconv 优于 fmt

  • 避免 string 到 byte 的转换

  • 代码样式

  • 聚合相似的声明

  • 包的分组导入的顺序

  • 包命名

  • 函数命名

  • 别名导入

  • 函数分组和顺序

  • 减少嵌套

  • 不必要的 else

  • 顶层变量的声明

  • 在不可导出的全局变量前面加上 _

  • 结构体的嵌入

  • 使用字段名去初始化结构体

  • 局部变量声明

  • nil 是一个有效的 slice

  • 减少变量的作用域

  • 避免裸参数

  • 使用原生字符串格式来避免转义

  • 初始化结构体

  • 在 Printf 之外格式化字符串

  • Printf-style 函数的命名

  • 设计模式

  • 表格驱动测试

  • 函数参数可选化

介绍

代码风格是代码的一种约定。用风格这个词可能有点不恰当,因为这些约定涉及到的远比源码文件格式工具 gofmt 所能处理的更多。

本指南的目标是通过详细描述 Uber 在编写 Go 代码时的取舍来管理代码的这种复杂性。这些规则的存在是为了保持代码库的可管理性,同时也允许工程师更高效地使用 go 语言特性。

本指南最初由 Prashant Varanasi 和 Simon Newton 为了让同事们更便捷地使用 go 语言而编写。多年来根据其他人的反馈进行了一些修改。

本文记录了 uber 在使用 go 代码中的一些习惯用法。许多都是 go 语言常见的指南,而其他的则延伸到了一些外部资料:

  1. Effective Go

  2. The Go common mistakes guide

所用的代码在运行 golint 和 go vet 之后不会有报错。建议将编辑器设置为:

  • 保存时运行 goimports

  • 运行 golint 和 go vet 来检查错误

你可以在下面的链接找到 Go tools 对一些编辑器的支持:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

指南

接口的指针

你几乎不需要指向接口的指针,应该把接口当作值传递,它的底层数据仍然可以当成一个指针。

一个接口是两个字段:

  1. 指向特定类型信息的指针。你可以认为这是 "type."。

  2. 如果存储的数据是指针,则直接存储。如果数据存储的是值,则存储指向此值的指针。

如果你希望接口方法修改底层数据,则必须使用指针。

接收者和接口

具有值接收者的方法可以被指针和值调用。

例如,

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"}} // 使用值只能调用 Read 方法
sVals[1].Read() // 会编译失败
//  sVals[0].Write("test")    sPtrs := map[int]*S{1: {"A"}}    // 使用指针可以调用 Read 和 Write 方法
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  // 以下不能被编译,因为 s2Val 是一个值,并且 f 没有值接收者
//   i = s2Val

Effective Go 对 Pointers vs. Values 分析的不错.

零值 Mutexes 是有效的

零值的 sync.Mutex 和 sync.RWMutex 是有效的,所以你几乎不需要指向 mutex 的指针。

var mu sync.Mutex   mu.Lock()
defer mu.Unlock()

如果你使用一个指针指向的结构体,mutex 可以作为一个非指针字段,或者,最好是直接嵌入这个结构体。

复制 Slice 和 Map

slice 和 map 包含指向底层数据的指针,因此复制的时候需要当心。

接收 Slice 和 Map 作为入参

需要留意的是,如果你保存了作为参数接收的 map 或 slice 的引用,可以通过引用修改它。

返回 Slice 和 Map

类似的,当心 map 或者 slice 暴露的内部状态是可以被修改的。

Defer 的使用

使用 defer 去关闭文件句柄和释放锁等类似的这些资源。

defer 的开销非常小,只有在你觉得你的函数执行需要在纳秒级别的情况下才需要考虑避免使用。使用 defer 换取的可读性是值得的。这尤其适用于具有比简单内存访问更复杂的大型方法,这时其他的计算比 defer 更重要。

channel 的大小是 1 或者 None

channel 的大小通常应该是 1 或者是无缓冲的。默认情况下,channel 是无缓冲的且大小为 0。任何其他的大小都必须经过仔细检查。应该考虑如何确定缓冲的大小,哪些因素可以防止 channel 在负载时填满和阻塞写入,以及当这种情况发生时会造成什么样的影响。

枚举值从 1 开始

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

在某些情况下,使用零值是有意义的,例如零值是想要的默认值。

type LogOutput int  const ( LogToStdout LogOutput = iota   LogToFile   LogToRemote
)   // LogToStdout=0, LogToFile=1, LogToRemote=2

Error 类型

声明 error 有多种选项:

  • errors.New] 声明简单静态的字符串

  • fmt.Errorf] 声明格式化的字符串

  • 实现了 Error() 方法的自定义类型

  • 使用 [ "pkg/errors".Wrap] 包装 error

返回 error 时,可以考虑以下因素以确定最佳选择:

  • 不需要额外信息的一个简单的 error? 那么 [ errors.New] 就够了

  • 客户端需要检查并处理这个 error?那么应该使用实现了 Error() 方法的自定义类型

  • 是否需要传递下游函数返回的 error?那么请看 section on error wrapping

  • 否则, 可以使用 [ fmt.Errorf]

如果客户端需要检查这个 error,你需要使用 [ errors.New] 和 var 来创建一个简单的 error。

如果你有一个 error 可能需要客户端去检查,并且你想增加更多的信息(例如,它不是一个简单的静态字符串),这时候你需要使用自定义类型。

在直接导出自定义 error 类型的时候需要小心,因为它已经是包的公共 API。最好暴露一个 matcher 函数(译者注:以下示例的 IsNotFoundError 函数)去检查 error。

// 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")    }
}

Error 包装

如果调用失败,有三个主要选项用于 error 传递:

  • 如果没有额外增加的上下文并且你想维持原始 error 类型,那么返回原始 error

  • 使用 [ "pkg/errors".Wrap] 增加上下文,以至于 error 信息提供更多的上下文,并且 [ "pkg/errors".Cause] 可以用来提取原始 error

  • 如果调用者不需要检查或者处理具体的 error 例子,那么使用 [ fmt.Errorf]

推荐去增加上下文信息取代描述模糊的 error,例如 "connection refused",应该返回例如 "failed to

请参考 Don't just check errors, handle them gracefully.

处理类型断言失败

简单的返回值形式的类型断言在断言不正确的类型时将会 panic。因此,需要使用 ", ok" 的常用方式。

避免 Panic

生产环境跑的代码必须避免 panic。它是导致 级联故障 的主要原因。如果一个 error 产生了,函数必须返回 error 并且允许调用者决定是否处理它。

panic/recover 不是 error 处理策略。程序在发生不可恢复的时候会产生 panic,例如对 nil 进行解引用。一个例外是在程序初始化的时候:在程序启动时那些可能终止程序的问题会造成 panic。

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

甚至在测试用例中,更偏向于使用 t.Fatal 或者 t.FailNow 解决 panic 确保这个测试被标记为失败。

使用 go.uber.org/atomic

使用 sync/atomic 对原生类型(例如, int32, int64)进行原子操作的时候,很容易在读取或者修改变量的时候忘记使用原子操作。

go.uber.org/atomic 通过隐藏底层类型使得这些操作是类型安全的。此外,它还包含一个比较方便的 atomic.Bool 类型。

性能

指定的性能指南仅适用于 hot path(译者注:hot path 指频繁执行的代码路径)

strconv 优于 fmt

对基本数据类型的字符串表示的转换, strconv 比fmt 速度快。

避免 string 到 byte 的转换

不要重复用固定 string 创建 byte slice。相反,执行一次转换后保存结果,避免重复转换。

代码风格

聚合相似的声明

Go 支持分组声明。

也能应用于常量,变量和类型的声明。

只需要对相关类型进行分组声明。不相关的不需要进行分组声明。

分组不受限制。例如,我们可以在函数内部使用它们。

包的分组导入的顺序

有两个导入分组:

  • 标准库

  • 其他库

这是默认情况下 goimports 应用的分组。

包命名

当给包命名的时候,可以参考以下方法,

  • 都是小写字母。没有大写字母或者下划线

  • 在大多数场景下没必要重命名包

  • 简明扼要。记住,每次调用时都会通过名称来识别。

  • 不要复数。例如,要使用 net/url, 不要使用 net/urls

  • 不要使用 "common", "util", "shared", "lib" 诸如此类的命名。这种方式不太好,无法从名字中获取有效信息。

也可以参考 Package Names 和 Style guideline for Go packages.

函数命名

我们遵循 Go 社区的习惯方法,使用驼峰法命名函数。测试函数是个例外,包含下划线是为了分组相关的测试用例。例如, TestMyFunction_WhatIsBeingTested

别名导入

如果包名和导入路径的最后一个元素不匹配,则要使用别名导入。

import (   "net/http"    client "example.com/client-go"    trace "example.com/trace/v2"
)

在大部分场景下,除非导入的包有直接的冲突,应该避免使用别名导入。

函数分组和顺序

  • 函数应该按大致的调用顺序排序

  • 同一个文件的函数应该按接收者分组

因此,导出的函数应该在 struct, const, var 定义之后。

newXYZ()NewXYZ() 应该在类型定义之后,并且在接收者的其余的方法之前出现。

因为函数是按接收者分组的,所以普通的函数应该快到文件末尾了。

减少嵌套

在可能的情况下,代码应该通过先处理 错误情况 / 特殊条件 并提前返回或继续循环来减少嵌套。

不必要的 else

如果在 if 的两个分支中都设置同样的变量,则可以用单个 if 替换它。

顶层变量的声明

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

如果表达式的类型与请求的类型不完全匹配,请指定类型。

type myError struct{}  func (myError) Error() string { return "error" }  func F() myError { return myError{} }   var _e error = F()
// F 返回了一个 myError 类型的对象,但是我们想要 error

在不可导出的全局变量前面加上 _

在不可导出的顶层 var 和 const 的前面加上 _,以便明确它们是全局符号。

特例:不可导出的 error 值前面应该加上 err 前缀。

理论依据:顶层变量和常量有一个包作用域。使用通用的名称很容易在不同的文件中意外地使用错误的值

结构体的嵌入

嵌入的类型(例如 mutex)应该在结构体字段的头部,并且在嵌入字段和常规字段间保留一个空行来隔离。

使用字段名去初始化结构体

当初始化结构体的时候应该指定字段名称,现在在使用 [ go vet] 的情况下是强制性的。

特例:当有 3 个或更少的字段时,可以在测试表中省略字段名。

tests := []struct{
}{  op Operation    want string
}{  {Add, "add"}, {Subtract, "subtract"},
}

局部变量声明

短变量声明( :=)应该被使用在有明确值的情况下。

然而,使用 var 关键字在某些情况下会让默认值更清晰,声明空 Slice,例如

nil 是一个有效的 slice

nil 是一个长度为 0 的 slice。意思是,

  • 使用 nil 来替代长度为 0 的 slice 返回

  • 检查一个空 slice,应该使用 len(s) == 0,而不是 nil

  • The zero value (a slice declared with var) is usable immediately withoutmake().

  • 零值(通过 var 声明的 slice)是立马可用的,并不需要 make() 。

减少变量的作用域

在没有 减少嵌套 相冲突的情况下,尽量减少变量的作用域。

如果在 if 之外需要函数调用的结果,则不要缩小作用域。

避免裸参数

函数调用中的裸参数不利于可读性。当参数名的含义不明显时,添加 C 语言风格的注释( /*…*/)。

更好的方法是,用自定义类型替换裸 bool 类型,以获得更可读的和类型安全的代码。这使得该参数未来的状态是可以增加的,不仅仅是两种(true/false)。

type Region int    const ( UnknownRegion Region = iota    Local
)   type Status int const ( StatusReady = iota + 1    StatusDone  // 可能未来我们将有一个 StatusInProgress 的状态
)   func printInfo(name string, region Region, status Status)

使用原生字符串格式来避免转义

Go 支持 原生字符串格式 ,它可以跨越多行并包含引号。使用这些来避免手动转义的字符串,因为手动转义的可读性很差。

初始化结构体

在初始化结构体的时候使用 &T{} 替代 new(T),以至于结构体初始化是一致的。

在 Printf 之外格式化字符串

如果你在 Printf 风格函数的外面声明一个格式化字符串,请使用 const 值。

这有助于 go vet 对格式化字符串执行静态分析。

Printf-style 函数的命名

当你声明一个 Printf 风格的函数,请确认 go vet 能够发现并检查这个格式化字符串。

这意味着你应该尽可能为 Printf 风格的函数名进行预定义 。 go vet 默认会检查它们。查看 Printf family 获取更多信息。

如果预定义函数名不可取,请用 f 作为名字的后缀即 wrapf,而不是 wrap。 go vet 可以检查特定的 printf 风格的名称,但它们必须以 f 结尾。

$ go vet -printfuncs=wrapf,statusf

请参考 go vet: Printf family check。

设计模式

表格驱动测试

当核心测试逻辑重复的时候,用 subtests 做表格驱动测试(译者注:table-driven tests 即 TDT 表格驱动方法)可以避免重复的代码。

Bad
// func TestSplitHostPort(t *testing.T)  host, port, err := net.SplitHostPort("192.0.2.0:8000")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "8000", port) host, port, err = net.SplitHostPort("192.0.2.0:http")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "http", port) host, port, err = net.SplitHostPort(":8000")
require.NoError(t, err)
assert.Equal(t, "", host)
assert.Equal(t, "8000", port) host, port, err = net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t, "1", host)
assert.Equal(t, "8", port)
Good
// func TestSplitHostPort(t *testing.T)  tests := []struct{ give     string wantHost string wantPort string
}{  {   give:     "192.0.2.0:8000",   wantHost: "192.0.2.0",    wantPort: "8000", },  {   give:     "192.0.2.0:http",   wantHost: "192.0.2.0",    wantPort: "http", },  {   give:     ":8000",    wantHost: "", wantPort: "8000", },  {   give:     "1:8",  wantHost: "1",    wantPort: "8",    },
}   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)  })
}

表格驱动测试使向错误消息添加上下文、减少重复逻辑和添加新测试用例变得更容易。

我们遵循这样一种约定,即结构体 slice 被称为 tests,每个测试用例被称为 tt。此外,我们鼓励使用 give 和 want 前缀解释每个测试用例的输入和输出值。

tests := []struct{ give     string wantHost string wantPort string
}{  // ...
}   for _, tt := range tests { // ...
}

函数参数可选化

函数参数可选化(functional options)是一种模式,在这种模式中,你可以声明一个不确定的 Option 类型,该类型在内部结构体中记录信息。函数接收可选化的参数,并根据在结构体上记录的参数信息进行操作

将此模式用于构造函数和其他需要扩展的公共 API 中的可选参数,特别是在这些函数上已经有三个或更多参数的情况下。

Bad
// package db func Connect(   addr string,    timeout time.Duration,  caching bool,
) (*Connection, error) {    // ...
}   // timeout 和 caching 必须要提供,哪怕用户想使用默认值    db.Connect(addr, db.DefaultTimeout, db.DefaultCaching)
db.Connect(addr, newTimeout, db.DefaultCaching)
db.Connect(addr, db.DefaultTimeout, false /* caching */)
db.Connect(addr, newTimeout, false /* caching */)
Good
type options struct {  timeout time.Duration   caching bool
}   // Option 重写 Connect.
type Option interface { apply(*options)
}   type optionFunc func(*options)  func (f optionFunc) apply(o *options) { f(o)
}   func WithTimeout(t time.Duration) Option {  return optionFunc(func(o *options) {    o.timeout = t  })
}   func WithCaching(cache bool) Option {   return optionFunc(func(o *options) {    o.caching = cache  })
}   // Connect 创建一个 connection
func Connect(   addr string,    opts ...Option,
) (*Connection, error) {    options := options{    timeout: defaultTimeout,    caching: defaultCaching,    }   for _, o := range opts {   o(&options) }   // ...
}   // Options 只在需要的时候提供    db.Connect(addr)
db.Connect(addr, db.WithTimeout(newTimeout))
db.Connect(addr, db.WithCaching(false))
db.Connect( addr,   db.WithCaching(false),  db.WithTimeout(newTimeout),
)

请参考,

  • Self-referential functions and the design of options

  • Functional options for friendly APIs

GoHack 2019 火热招募中!

GoHack,一所专属于 Gopher 的黑客训练营。如果你有着 Golang 的奇思妙想,愿意带着伙伴们一起 Go to change the world,那就快来加入我们吧!这里有大咖导师的保驾护航,多家公司的资源助力,Geek高效的开发环境,以及众多志同道合者的头脑风暴,你的 idea 必将落地生根!

本次活动成绩由评委共同投票评奖(专业分+观众分),评出各类奖项。

一等奖:10000 RMB

二等奖:5000 RMB

三等奖:3000 RMB

另设有最受观众喜爱奖、最佳创意奖、最优雅实现奖若干

等你来收入囊中!

报名请请戳:阅读原文

Go中国

扫码关注

国内最具规模和生命力的 Go 开发者社区

【重磅】Uber Go 语言代码风格指南相关推荐

  1. 来自 Google 的 R 语言编码风格指南

    来自 Google 的 R 语言编码风格指南 R 语言是一门主要用于统计计算和绘图的高级编程语言. 这份 R 语言编码风格指南旨在让我们的 R 代码更容易阅读.分享和检查. 以下规则系与 Google ...

  2. Python 代码风格指南谷歌版

    非常感谢我们的忠实读者 shendeguize,在后台留言告诉我,已经翻译了<谷歌Python代码风格指南> ,大家这样相互帮助,感觉真是太好. Update: 2020.01.31 Tr ...

  3. 数据简化社区Google和Linux代码风格指南(附PDF公号发“代码风格”下载)

    数据简化社区Google和Linux代码风格指南(附PDF公号发"代码风格"下载) 秦陇纪2019代码类 数据简化DataSimp 昨天 数据简化DataSimp导读:数据简化社区 ...

  4. python代码风格指南_记录Python代码:完整指南

    python代码风格指南 Welcome to your complete guide to documenting Python code. Whether you're documenting a ...

  5. PEP8 - Python 代码风格指南中英对照

    PEP8 - Python 代码风格指南中英对照 Introduction A Foolish Consistency is the Hobgoblin of Little Minds Code la ...

  6. 汇编程序员之代码风格指南

    Style Guidelines for Assembly Language Programmers 汇编程序员之代码风格指南 作者:Randall Hyde   http://webster.cs. ...

  7. Google 内部的 Python 代码风格指南(译)

    微信搜索逆锋起笔关注后回复编程pdf 领取编程大佬们所推荐的 23 种编程资料! 来自:Why GitHub? 链接:https://github.com/shendeguize/GooglePyth ...

  8. Google内部 Python 代码风格指南(中文版)

    文末有干货 "Python高校",马上关注 真爱,请置顶或星标 这是一位大佬翻译的Google Python代码风格指南,很全面.可以作为公司的code review 标准,也可以 ...

  9. 快快快收藏!!Google内部Python代码风格指南(中文版)

    ????????关注后回复 "进群" ,拉你进程序员交流群???????? 来源丨菜鸟学Python 这是一位大佬翻译的Google Python代码风格指南,很全面.可以作为公司 ...

最新文章

  1. 项目松弛时期 团队如何休养生息?
  2. C# 实现单线程线程池并调用实例
  3. Android 系统中 Location Service 的实现与架构
  4. 6.0 《数据库系统概论》之关系数据库的规范化理论(数据依赖对表的影响[插入-删除-修改-冗余]、1NF-2NF-3NF-BCNF-4NF、函数依赖与多值依赖)
  5. 折线图表android,Android 折线图表MPAndroidChart的实现
  6. web---SSL/TSL
  7. 使用Entity Framework Core,Swagger和Postman创建ASP.NET Core Web API的分步指南
  8. 基于JAVA+SSH+MYSQL的鲜花订购系统
  9. 让 API 端点的响应速度提高 50 倍!
  10. 真机调试 —— An unknown error occurred.
  11. sun服务器如何查cpu信息,solaris 如何查看CPU信息
  12. Oracle 字段 中文英文拆分
  13. jfinal,jxl导出excel遇到的异常
  14. 高并发限流-漏桶算法和令牌桶算法
  15. 云上发展,唯快不破!IT部门是数字化转型的变革者 | 凌云时刻
  16. 微软的sdk以及azure_.NET的Azure SDK:关于困难错误搜索的故事
  17. Excel —— 相对引用录制宏(附视频)
  18. Python人脸识别签到考勤系统
  19. android浏览器和iPhone浏览器
  20. 国家版权中心软件著作权网站注册不了的bug修复

热门文章

  1. 企业人力资源主要是负责什么,有哪些工作内容
  2. 【华为上机】天天爱消除
  3. 简述java语言特点
  4. 基于php校园BBS论坛网站
  5. MySQL配置SSL加密连接
  6. darknet框架 VS2017 平台工具集141_Nvidia推出Omniverse平台,可远程进行2D/3D多软件实时协作...
  7. 深度学习-DRGAN对抗神经网络生成动漫头像
  8. 文献阅读|找到Science Direct的RSS订阅
  9. 【超图】SuperMap iClient3D for WebGL 加载TMS瓦片
  10. 《SpringBoot篇》26.SpringBoot整合Jackson超详细教程(附Jackson工具类)