Uber Go 语言编程规范
相信很多人前两天都看到 Uber 在 github 上面开源的 Go 语言编程规范了,原文在这里:https://github.com/uber-go/guide/blob/master/style.md 。我们今天就来简单了解一下国外大厂都是如何来写代码的。行文仓促,错误之处,多多指正。另外如果觉得还不错,也欢迎分享给更多的人。
文章目录
-
- Uber Go 语言编程规范
-
- 1. 介绍
- 2. 编程指南
- 3. 性能相关
- 4. 编程风格
-
- 4.1 声明语句分组
- 4.2 package 命名
- 4.3 函数命名
- 4.4 import 别名
- 4.5 函数分组和排序
- 4.6 避免代码块嵌套
- 4.7 避免不必要的 else 语句
- 4.8 两级 (two-level) 变量声明
- 4.9 对于不做 export 的全局变量使用前缀 _
- 4.10 struct 嵌套
- 4.11 struct 初始化的时候带上 Field
- 4.12 局部变量声明
- 4.13 nil 是合法的 slice
- 4.14 避免 scope
- 4.15 避免参数语义不明确(Avoid Naked Parameters)
- 4.16 使用原生字符串,避免转义
- 4.17 Struct 引用初始化
- 4.18 字符串 string format
- 4.19 Printf 风格函数命名
- 5. 编程模式(Patterns)
- 6. 总结
1. 介绍
英文原文标题是 Uber Go Style Guide,这里的 Style 是指在管理我们代码的时候可以遵从的一些约定。
这篇编程指南的初衷是更好的管理我们的代码,包括去编写什么样的代码,以及不要编写什么样的代码。我们希望通过这份编程指南,代码可以具有更好的维护性,同时能够让我们的开发同学更高效地编写 Go 语言代码。
这份编程指南最初由 Prashant Varanasi 和 Simon Newton 编写,旨在让其他同事快速地熟悉和编写 Go 程序。经过多年发展,现在的版本经过了多番修改和改进了。这是我们在 Uber 遵从的编程范式,但是很多都是可以通用的,如下是其他可以参考的链接:
所有的提交代码都应该通过 golint
和 go vet
检测,建议在代码编辑器上面做如下设置:
- 保存的时候运行
goimports
- 使用
golint
和go vet
去做错误检测。
你可以通过下面链接发现更多的 Go 编辑器的插件: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
2. 编程指南
2.1 指向 Interface 的指针
在我们日常使用中,基本上不会需要使用指向 interface 的指针。当我们将 interface 作为值传递的时候,底层数据就是指针。Interface 包括两方面:
- 一个包含 type 信息的指针
- 一个指向数据的指针
如果你想要修改底层的数据,那么你只能使用 pointer。
2.2 Receiver 和 Interface
使用值作为 receiver 的时候 method 可以通过指针调用,也可以通过值来调用。
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")
相似的,pointer 也可以满足 interface 的要求,尽管 method 使用 value 作为 receiver。
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 关于如何使用指针和值也有一些不错的 practice: Pointers vs. Values.
2.3 mutex 默认 0 值是合法的
sync.Mutex
和 sync.RWMutex
的 0 值也是合法的,所以我们基本不需要声明一个指针指向 mutex。
Bad
mu := new(sync.Mutex)
mu.Lock()
Good
var mu sync.Mutex
mu.Lock()
如果 struct 内部使用 mutex,在我们使用 struct 的指针类型时候,mutex 也可以是一个非指针类型的 field,或者直接嵌套在 struct 中。
Mutex 直接嵌套在 struct 中。
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]
}
将 Mutex 作为一个 struct 内部一个非指针类型 Field 使用。
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]
}
2.4 拷贝 Slice 和 Map
Slice 和 Map 都包含了对底层存储数据的指针,所以注意在修改 slice 或者 map 数据的场景下,是不是使用了引用。
slice 和 map 作为参数
当把 slice 和 map 作为参数的时候,如果我们对 slice 或者 map 的做了引用操作,那么修改会修改掉原始值。如果这种修改不是预期的,那么要先进行 copy。
Bad
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// Did you mean to modify d1.trips?
trips[0] = ...
Good
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] = ...
slice 和 map 作为返回值
当我们的函数返回 slice 或者 map 的时候,也要注意是不是直接返回了内部数据的引用到外部。
Bad
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()
Good
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()
2.5 使用 defer 做资源清理
建议使用 defer 去做资源清理工作,比如文件,锁等。
Bad
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
Good
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// more readable
尽管使用 defer 会导致一定的性能开销,但是大部分情况下这个开销在你的整个链路上所占的比重往往是微乎其微,除非说真的是有非常高的性能需求。另外使用 defer 带来的代码可读性的改进以及减少代码发生错误的概率都是值得的。
2.6 channel 的 size 最好是 1 或者是 unbuffered
在使用 channel 的时候,最好将 size 设置为 1 或者使用 unbuffered channel。其他 size 的 channel 往往都会引入更多的复杂度,需要更多考虑上下游的设计。