相信很多人前两天都看到 Uber 在 github 上面开源的 Go 语言编程规范了,原文在这里:https://github.com/uber-go/guide/blob/master/style.md 。我们今天就来简单了解一下国外大厂都是如何来写代码的。行文仓促,错误之处,多多指正。另外如果觉得还不错,也欢迎分享给更多的人。
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 可以通过指针调用,也可以通过值来调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
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"}}
|
相似的,pointer 也可以满足 interface 的要求,尽管 method 使用 value 作为 receiver。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
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
|
Effective Go 关于如何使用指针和值也有一些不错的 practice: Pointers vs. Values.
2.3 mutex 默认 0 值是合法的
sync.Mutex 和 sync.RWMutex 的 0 值也是合法的,所以我们基本不需要声明一个指针指向 mutex。
Bad
1 2
|
mu := new(sync.Mutex) mu.Lock()
|
Good
1 2
|
var mu sync.Mutex mu.Lock()
|
如果 struct 内部使用 mutex,在我们使用 struct 的指针类型时候,mutex 也可以是一个非指针类型的 field,或者直接嵌套在 struct 中。
Mutex 直接嵌套在 struct 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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 使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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
1 2 3 4 5 6 7 8 9
|
func (d *Driver) SetTrips(trips []Trip) { d.trips = trips }
trips := ... d1.SetTrips(trips)
|
Good
1 2 3 4 5 6 7 8 9 10
|
func (d *Driver) SetTrips(trips []Trip) { d.trips = make([]Trip, len(trips)) copy(d.trips, trips) }
trips := ... d1.SetTrips(trips)
|
slice 和 map 作为返回值
当我们的函数返回 slice 或者 map 的时候,也要注意是不是直接返回了内部数据的引用到外部。
Bad
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
type Stats struct { sync.Mutex
counters map[string]int }
|
Good
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
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 }
|
2.5 使用 defer 做资源清理
建议使用 defer 去做资源清理工作,比如文件,锁等。
Bad
1 2 3 4 5 6 7 8 9 10 11 12 13
|
p.Lock() if p.count < 10 { p.Unlock() return p.count }
p.count++ newCount := p.count p.Unlock()
return newCount
|
Good
1 2 3 4 5 6 7 8 9 10 11
|
p.Lock() defer p.Unlock()
if p.count < 10 { return p.count }
p.count++ return p.count
|
尽管使用 defer 会导致一定的性能开销,但是大部分情况下这个开销在你的整个链路上所占的比重往往是微乎其微,除非说真的是有非常高的性能需求。另外使用 defer 带来的代码可读性的改进以及减少代码发生错误的概率都是值得的。
2.6 channel 的 size 最好是 1 或者是 unbuffered
在使用 channel 的时候,最好将 size 设置为 1 或者使用 unbuffered channel。其他 size 的 channel 往往都会引入更多的复杂度,需要更多考虑上下游的设计。
Bad
Good
2.7 枚举变量应该从 1 开始
在 Go 语言中枚举值的声明典型方式是通过 const 和 iota 来声明。由于 0 是默认值,所以枚举值最好从一个非 0 值开始,比如 1。
Bad
1 2 3 4 5 6 7 8 9
|
type Operation int
const ( Add Operation = iota Subtract Multiply )
|
Good
1 2 3 4 5 6 7 8 9
|
type Operation int
const ( Add Operation = iota + 1 Subtract Multiply )
|
有一种例外情况:0 值是预期的默认行为的时候,枚举值可以从 0 开始。
1 2 3 4 5 6 7 8 9
|
type LogOutput int
const ( LogToStdout LogOutput = iota LogToFile LogToRemote )
|
2.8 Error 类型
在 Go 语言中声明 error 可以有多种方式:
-
errors.New 声明包含简单静态字符串的 error
-
fmt.Errorf 格式化 error string
- 其他自定义类型使用了
Error() 方法
- 使用
"pkg/errors".Wrap
当要把 error 作为返回值的时候,可以考虑如下的处理方式
- 是不是不需要额外信息,如果是,
errors.New 就足够了。
- client 需要检测和处理返回的 error 吗?如果是,最好使用实现了
Error() 方法的自定义类型,这样可以包含更多的信息。
- error 是不是从下游函数传递过来的?如果是,考虑一下 error wrap,参考:section on error wrapping.
- 其他情况,
fmt.Errorf 一般足够了。
对于 client 需要检测和处理 error 的情况,这里详细说一下。如果你要通过 errors.New 声明一个简单的 error,那么可以使用一个变量声明:var ErrCouldNotOpen = errors.New("Could not open")
Bad
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
|
Good
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
|
如果需要 error 中包含更多的信息,而不仅仅类型原生 error 的这种简单字符串,那么最好使用一个自定义类型。
Bad
1 2 3 4 5 6 7 8 9 10 11 12 13
|
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") {
|
Good
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
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 {
|
在直接暴露自定义的 error 类型的时候,最好 export 配套的检测自定义 error 类型的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
|
2.9 Error Wrapping
在函数调用失败的时候,有三种方式可以将下游的 error 传递出去:
- 直接返回失败函数返回的 error。
- 使用
"pkg/errors".Wrap 增加更多的上下文信息,这种情况下可以使用 "pkg/errors".Cause 去提取原始的 error 信息。
- 如果调用者不需要检测和处理返回的 error 信息的话,可以直接使用
fmt.Errorf 将需要附加的信息进行格式化添加进去。
如果条件允许,最好增加上下文信息。比如 “connection refused” 和 “call service foo: connection refused” 这两种错误信息在可读性上比较也是高下立判。当增加上下文信息的时候,尽量保持简洁。比如像 “failed to” 这种极其明显的信息就没有必要写上去了。
Bad
1 2 3 4 5
|
s, err := store.New() if err != nil { return fmt.Errorf( "failed to create new store: %s", err) }
|
Good
1 2 3 4 5
|
s, err := store.New() if err != nil { return fmt.Errorf( "new store: %s", err) }
|
另外对于需要传播到其他系统的 error,也要有明显的标识信息,比如在 log 的最前面增加 err 等字样。
更多参考:Don’t just check errors, handle them gracefully.
2.10 类型转换失败处理
类型转换失败会导致进程 panic,所以对于类型转换,一定要使用 “comma ok” 的范式来处理。
Bad
Good
1 2 3 4
|
t, ok := i.(string) if !ok {
|
2.11 不要 panic
对于线上环境要尽量避免 panic。在很多情况下,panic 都是引起雪崩效应的罪魁祸首。一旦 error 发生,我们应该向上游调用者返回 error,并且容许调用者对 error 进行检测和处理。
Bad
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
func foo(bar string) { if len(bar) == 0 { panic("bar must not be empty") }
|
Good
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
func foo(bar string) error { if len(bar) == 0 return errors.New("bar must not be empty") }
|
Panic/Recover 并不是一种 error 处理策略。进程只有在某些不可恢复的错误发生的时候才需要 panic。
在跑 test case 的时候,使用 t.Fatal 或者 t.FailNow ,而不是 panic 来保证这个 test case 会被标记为失败的。
Bad
Good
2.12 使用 go.uber.org/atomic
这个是 Uber 内部对原生包 sync/atomic 的一种封装,隐藏了底层数据类型。
Bad
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
type foo struct { running int32
|
Good
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
type foo struct { running atomic.Bool }
func (f *foo) start() { if f.running.Swap(true) {
|
3. 性能相关
3.1 类型转换时,使用 strconv 替换 fmt
当基本类型和 string 互转的时候,strconv 要比 fmt 快。
Bad
1 2 3 4 5
|
for i := 0; i < b.N; i++ { s := fmt.Sprint(rand.Int()) }
BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
|
Good
1 2 3 4 5
|
for i := 0; i < b.N; i++ { s := strconv.Itoa(rand.Int()) }
BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
|
3.2 避免 string to byte 的不必要频繁转换
在通过 string 创建 byte slice 的时候,不要在循环语句中重复的转换,而是要将重复的转换逻辑提到循环外面,做一次即可。(看上去很 general 的建议)
Bad
1 2 3 4 5
|
for i := 0; i < b.N; i++ { w.Write([]byte("Hello world")) }
BenchmarkBad-4 50000000 22.2 ns/op
|
Good
1 2 3 4 5 6
|
data := []byte("Hello world") for i := 0; i < b.N; i++ { w.Write(data) }
BenchmarkGood-4 500000000 3.25 ns/op
|
4. 编程风格
4.1 声明语句分组
import 语句分组
Bad
Good
常量、变量以及 type 声明
Bad
1 2 3 4 5 6 7 8
|
const a = 1 const b = 2
var a = 1 var b = 2
type Area float64 type Volume float64
|
Good
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
const ( a = 1 b = 2 )
var ( a = 1 b = 2 )
type ( Area float64 Volume float64 )
|
import 根据导入的包进行顺序分组。(其他库我们其实可以再细分 private 库和 public 库)
Bad
1 2 3 4 5 6
|
import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" )
|
Good
1 2 3 4 5 6 7
|
import ( "fmt" "os"
"go.uber.org/atomic" "golang.org/x/sync/errgroup" )
|
4.2 package 命名
package 命名的几条规则:
- 全小写。不包含大写字母或者下划线。
- 简洁。
- 不要使用复数。比如,使用
net/url ,而不是 net/urls 。
- 避免:”common”, “util”, “shared”, “lib”,不解释。
更多参考:
4.3 函数命名
函数命名遵从社区规范: MixedCaps for fun |
请发表评论