Uber Go 风格指南(译) - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
xuxu555
V2EX    程序员

Uber Go 风格指南(译)

  •  1
     
  •   xuxu555
    Allenxuxu 2019-10-14 10:36:31 +08:00 3131 次点击
    这是一个创建于 2188 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Uber Go 风格指南

    简介

    风格是指规范代码的共同约定。风格一词其实是有点用词不当的,因为共同约定的范畴远远不止 gofmt 所做的源代码格式化这些。

    本指南旨在通过详尽描述 Uber 在编写 Go 代码中的注意事项(规定)来解释其中复杂之处。制定这些注意事项(规定)是为了提高代码可维护性同时也让工程师们高效的使用 Go 的特性。

    这份指南最初由 Prashant Varanasi 和 Simon Newton 编写,目的是让一些同事快速上手 Go。多年来,已经根据其他人的反馈不断修改。

    这份文档记录了我们在 Uber 遵守的 Go 惯用准则。其中很多准则是 Go 的通用准则,其他方面依赖于外部资源:

    1. Effective Go
    2. The Go common mistakes guide

    所有的代码都应该通过 golintgo vet 检查。我们建议您设置编辑器:

    • 保存时自动运行 goimports
    • 自动运行 golintgo vet 来检查错误

    您可以在这找到关于编辑器设定 Go tools 的相关信息:

    https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

    指南

    指向接口( interface )的指针

    你基本永远不需要一个指向接口的指针。你应该直接将接口作为值传递,因为接口的底层数据就是指针。

    一个接口包含两个字段:

    1. 类型指针,指向某些特定类型信息的指针。
    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.Mutexsync.RWMutex 是有效的,所以基本是不需要一个指向 Mutex 的指针的。

    <thead></thead>
    BadGood
    mu := new(sync.Mutex) mu.Lock() 
    var mu sync.Mutex mu.Lock() 

    如果你希望通过指针操作结构体,mutex 可以作为其非指针结构体字段,或者最好直接嵌入结构体中。

    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] } 
    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] } 
    嵌入到非导出类型或者需要实现 Mutex 接口的类型。 对于导出类型,将 mutex 作为私有成员变量。

    Slices 和 Maps 的边界拷贝操作

    切片和 map 包含一个指针来指向底层数据,所以当需要复制他们时需要特别注意。

    接收 Slices 和 Maps

    请记住,如果存储了对 slice 或 map 的引用,那么用户是可以对其进行修改。

    <thead></thead>
    Bad Good
    func (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := ... d1.SetTrips(trips) // 是想修改 d1.trips 吗? trips[0] = ... 
    func (d *Driver) SetTrips(trips []Trip) { d.trips = make([]Trip, len(trips)) copy(d.trips, trips) } trips := ... d1.SetTrips(trips) // 修改 trips[0] 并且不影响 d1.trips。 trips[0] = ... 

    返回 Slices 和 Maps

    同理,谨慎提防用户修改暴露内部状态的 slices 和 maps。

    <thead></thead>
    BadGood
    type Stats struct { sync.Mutex counters map[string]int } // Snapshot 返回当前状态 func (s *Stats) Snapshot() map[string]int { s.Lock() defer s.Unlock() return s.counters } // snapshot 不再受锁保护了! snapshot := stats.Snapshot() 
    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 是一分拷贝的内容了 snapshot := stats.Snapshot() 

    使用 defer 来做清理工作

    使用 defer 来做资源的清理工作,例如文件的关闭和锁的释放。

    <thead></thead>
    BadGood
    p.Lock() if p.count < 10 { p.Unlock() return p.count } p.count++ newCount := p.count p.Unlock() return newCount // 当有多处 return 时容易忘记释放锁 
    p.Lock() defer p.Unlock() if p.count < 10 { return p.count } p.count++ return p.count // 可读性更高 

    defer 只有非常小的性能开销,只有当你能证明你的函数执行时间在纳秒级别时才可以不使用它。使用 defer 对代码可读性的提高是非常值得的,因为使用 defer 的成本真的非常小。特别是在一些主要是做内存操作的长函数中,函数中的其他计算操作远比 defer 重要。

    Channel 的大小设为 1 还是 None

    通道的大小通常应该设为 1 或者设为无缓冲类型。默认情况下,通道是无缓冲类型的,大小为 0。将通道大小设为其他任何数值都应该经过深思熟虑。认真考虑如何确定其大小,是什么阻止了工作中的通道被填满并阻塞了写入操作,以及何种情况会发生这样的现象。

    <thead></thead>
    BadGood
    // 足以满足任何人! c := make(chan int, 64) 
    // 大小 为 1 c := make(chan int, 1) // or // 无缓冲 channel, 大小为 0 c := make(chan int) 

    枚举类型值从 1 开始

    在 Go 中使用枚举的标准方法是声明一个自定义类型并通过 iota 关键字来声明一个 const 组。但是由于 Go 中变量的默认值都为该类型的零值,所以枚举变量的值应该从非零值开始。

    <thead></thead>
    BadGood
    type Operation int const ( Add Operation = iota Subtract Multiply ) // Add=0, Subtract=1, Multiply=2 
    type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) // Add=1, Subtract=2, Multiply=3 

    在某些情况下,从零值开始也是可以的。例如,当零值是我们期望的默认行为时。

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

    错误类型

    有很多种方法来声明 errors:

    • errors.New 声明简单的静态字符串错误信息
    • fmt.Errorf 声明格式化的字符串错误信息
    • 为自定义类型实现 Error() 方法
    • 通过 "pkg/errors".Wrap 包装错误类型

    返回错误时,请考虑以下因素来作出最佳选择:

    • 这是一个不需要其他额外信息的简单错误吗?如果是,使用error.New。
    • 客户需要检测并处理此错误吗?如果是,那应该自定义类型,并实现 Error() 方法。
    • 是否是在传递一个下游函数返回的错误?如果是,请查看error 封装部分。
    • 其他,使用 fmt.Errorf

    如果客户需要检测错误,并且是通过 errors.New 创建的一个简单的错误,请使用 var 声明这个错误类型。

    <thead></thead>
    BadGood
    // package foo func Open() error { return errors.New("could not open") } // package bar func use() { if err := foo.Open(); err != nil { if err.Error() == "could not open" { // handle } else { panic("unknown error") } } } 
    // package foo var ErrCouldNotOpen = errors.New("could not open") func Open() error { return ErrCouldNotOpen } // package bar if err := foo.Open(); err != nil { if err == foo.ErrCouldNotOpen { // handle } else { panic("unknown error") } } 

    如果你有一个错误需要客户端来检测,并且你想向其添加更多信息(例如,它不是一个简单的静态字符串),那么应该声明一个自定义类型。

    <thead></thead>
    BadGood
    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") { // handle } else { panic("unknown error") } } } 
    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 { // handle } else { panic("unknown error") } } } 

    直接将自定义的错误类型设为导出需要特别小心,因为这意味着他们已经成为包的公开 API 的一部分了。更好的方式是暴露一个匹配函数来检测错误。

    // 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 封装

    下面提供三种主要的方法来传递函数调用失败返回的错误:

    • 如果想要维护原始错误类型并且不需要添加额外的上下文信息,就直接返回原始错误。
    • 使用 "pkg/errors".Wrap 来增加上下文信息,这样返回的错误信息中就会包含更多的上下文信息,并且通过 "pkg/errors".Cause 可以提取出原始错误信息。
    • 如果调用方不需要检测或处理特定的错误情况,就直接使用 fmt.Errorf

    情况允许的话建议增加更多的上下文信息来代替诸如 "connection refused" 之类模糊的错误信息。返回 "failed to call service foo: connection refused" 用户可以知道更多有用的错误信息。

    在将上下文信息添加到返回的错误时,请避免使用 "failed to" 之类的短语以保持信息简洁,这些短语描述的状态是显而易见的,并且会随着错误在堆栈中的传递而逐渐堆积:

    <thead></thead>
    BadGood
    s, err := store.New() if err != nil { return fmt.Errorf( "failed to create new store: %s", err) } 
    s, err := store.New() if err != nil { return fmt.Errorf( "new store: %s", err) } 
    failed to x: failed to y: failed to create new store: the error 
    x: y: new store: the error 

    但是,如果这个错误信息是会被发送到另一个系统时,必须清楚的表明这是一个错误(例如,日志中 err 标签或者 Failed 前缀)。

    另见 Don't just check errors, handle them gracefully

    处理类型断言失败

    类型断言的单返回值形式在遇到类型错误时会直接 panic。因此,请始终使用 "comma ok" 惯用方法。

    <thead></thead>
    BadGood
    t := i.(string) 
    t, ok := i.(string) if !ok { // handle the error gracefully } 

    不要 Panic

    生产级的代码必须避免 panics。panics 是级联故障的主要源头。如果错误发生,函数应该返回错误并且允许调用者决定如果处理它。

    <thead></thead>
    BadGood
    func foo(bar string) { if len(bar) == 0 { panic("bar must not be empty") } // ... } func main() { if len(os.Args) != 2 { fmt.Println("USAGE: foo <bar>") os.Exit(1) } foo(os.Args[1]) } 
    func foo(bar string) error { if len(bar) == 0 return errors.New("bar must not be empty") } // ... return nil } func main() { if len(os.Args) != 2 { fmt.Println("USAGE: foo <bar>") os.Exit(1) } if err := foo(os.Args[1]); err != nil { panic(err) } } 

    Panic/recover 并不是错误处理策略。程序只有在遇到无法处理的情况下才可以 panic,例如,nil 引用。程序初始化时是一个例外情况:程序启动时遇到需要终止执行的错误可能会 painc。

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

    即使是在测试中,也应优先选择 t.Fatalt.FailNow 而非 panic,以确保测试标记为失败。

    <thead></thead>
    BadGood
    // func TestFoo(t *testing.T) f, err := ioutil.TempFile("", "test") if err != nil { panic("failed to set up test") } 
    // func TestFoo(t *testing.T) f, err := ioutil.TempFile("", "test") if err != nil { t.Fatal("failed to set up test") } 

    使用 go.uber.org/atomic

    Go 的 sync/atomic 包仅仅提供针对原始类型( int32, int64, ...)的原子操作。因此,很容易忘记使用原子操作来读写变量。

    go.uber.org/atomic 通过隐藏基础类型,使这些操作类型安全。并且,它还提供一个方便的 atomic.Bool 类型。

    <thead></thead>
    BadGood
    type foo struct { running int32 // atomic } func (f* foo) start() { if atomic.SwapInt32(&f.running, 1) == 1 { // already running… return } // start the Foo } func (f *foo) isRunning() bool { return f.running == 1 // race! } 
    type foo struct { running atomic.Bool } func (f *foo) start() { if f.running.Swap(true) { // already running… return } // start the Foo } func (f *foo) isRunning() bool { return f.running.Load() } 

    性能

    性能方面的特定准则,仅适用于热路径。

    strconv 性能优于 fmt

    将原语转换为字符串或从字符串转换时,strconv 速度比 fmt 更快。

    <thead></thead>
    BadGood
    for i := 0; i < b.N; i++ { s := fmt.Sprint(rand.Int()) } 
    for i := 0; i < b.N; i++ { s := strconv.Itoa(rand.Int()) } 
    BenchmarkFmtSprint-4 143 ns/op 2 allocs/op 
    BenchmarkStrconv-4 64.2 ns/op 1 allocs/op 

    避免 string to byte 的转换

    不要反复地从字符串字面量创建 byte 切片。相反,执行一次转换后存储结果供后续使用。

    <thead></thead>
    BadGood
    for i := 0; i < b.N; i++ { w.Write([]byte("Hello world")) } 
    data := []byte("Hello world") for i := 0; i < b.N; i++ { w.Write(data) } 
    BenchmarkBad-4 50000000 22.2 ns/op 
    BenchmarkGood-4 500000000 3.25 ns/op 

    代码风格

    声明分组

    Go 支持将相似的声明分组:

    <thead></thead>
    BadGood
    import "a" import "b" 
    import ( "a" "b" ) 

    分组同样适用于常量、变量和类型的声明:

    <thead></thead>
    BadGood
    const a = 1 const b = 2 var a = 1 var b = 2 type Area float64 type Volume float64 
    const ( a = 1 b = 2 ) var ( a = 1 b = 2 ) type ( Area float64 Volume float64 ) 

    仅将相似的声明放在同一组。不相关的声明不要放在同一个组内。

    <thead></thead>
    BadGood
    type Operation int const ( Add Operation = iota + 1 Subtract Multiply ENV_VAR = "MY_ENV" ) 
    type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) const ENV_VAR = "MY_ENV" 

    声明分组可以在任意位置使用。例如,可以在函数内部使用。

    <thead></thead>
    BadGood
    func f() string { var red = color.New(0xff0000) var green = color.New(0x00ff00) var blue = color.New(0x0000ff) ... } 
    func f() string { var ( red = color.New(0xff0000) green = color.New(0x00ff00) blue = color.New(0x0000ff) ) ... } 

    ...... v2ex 字数限制 20000, 更多内容,到博客或者 GitHub 仓库看吧:

    10 条回复    2019-10-14 12:26:17 +08:00
    wkzq
        1
    wkzq  
       2019-10-14 11:34:33 +08:00
    不错, 顶
    MrAcanyi
        2
    MrAcanyi  
       2019-10-14 11:46:55 +08:00
    哎,这不是一个公众号的内容吗。。。。
    sls
        3
    sls  
       2019-10-14 12:01:04 +08:00
    总有好心人
    xuxu555
        4
    xuxu555  
    OP
       2019-10-14 12:06:14 +08:00
    @MrAcanyi 哪个公众号,我看看
    Leigg
        5
    Leigg  
       2019-10-14 12:09:17 +08:00 via Android
    开发者头条发了,go 中国公众号发了
    labulaka521
        6
    labulaka521  
       2019-10-14 12:14:32 +08:00 via Android
    赞 ,但是 github 建议留个原链接表示尊重
    xuxu555
        7
    xuxu555  
    OP
       2019-10-14 12:16:01 +08:00
    @labulaka521 留了呀,在开头留了 GitHub 地址的
    labulaka521
        8
    labulaka521  
       2019-10-14 12:20:01 +08:00 via Android   1
    @xuxu555 我说的你那个仓库。
    xuxu555
        9
    xuxu555  
    OP
       2019-10-14 12:23:36 +08:00
    @labulaka521 已添加了,感谢提醒
    xuxu555
        10
    xuxu555  
    OP
       2019-10-14 12:26:17 +08:00
    @Leigg 我没有在这两个平台发过,应该是别的开发者翻译的吧。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5358 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 06:47 PVG 14:47 LAX 23:47 JFK 02:47
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86