GO代码风格指南 Uber Go (转载)

GO代码风格指南 Uber Go (转载),第1张

概述原文地址:https://github.com/uber-go/guide/blob/master/style.md 译文出处:https://github.com/uber-go/guide 本文永 原文地址:https://github.com/uber-go/guide/blob/master/style.md译文出处:https://github.com/uber-go/guide本文永久链接:https://github.com/gocn/translator/blob/master/2019/w38_uber_go_style_guide.md译者:咔叽咔叽校对者:fivezh,cvley目录介绍指南接口的指针接收者和接口零值 Mutexes 是有效的复制 Slice 和 MapDefer 的使用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 语言常见的指南,而其他的则延伸到了一些外部资料:

Effective GoThe Go common mistakes guide@H_404_133@

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

保存时运行 goimports运行 golint 和 go vet 来检查错误

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

指南接口的指针

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

一个接口是两个字段:

指向特定类型信息的指针。你可以认为这是 "type."。如果存储的数据是指针,则直接存储。如果数据存储的是值,则存储指向此值的指针。@H_404_133@

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

接收者和接口

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

例如,

type S struct {  data string}func (s S) Read()  {  return s.data}func (s *S) Write(str ) {  s.data = str}sVals := map[int]S{1: {"A"}}// 使用值只能调用 Read 方法sVals[1].Read() 会编译失败  sVals[0].Write("test")sPtrs := map[int]*S{ 使用指针可以调用 Read 和 Write 方法sPtrs[].Read()sPtrs[1].Write(test")

 

类似的,即使方法是一个值接收者,但接口仍可以被指针类型所满足。

type F interface {  f()}type S1 {}func (s S1) f() {}type S2 {}func (s *S2) f() {}s1Val := S1{}s1Ptr := &S1{}s2Val := S2{}s2Ptr := &S2{}var i Fi = s1Vali = s1Ptri = s2Ptr 以下不能被编译,因为 s2Val 是一个值,并且 f 没有值接收者   i = s2Val

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

零值 Mutexes 是有效的

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

BadGood
mu := new(sync.Mutex)
mu.Lock() mu sync.Mutexmu.Lock()

 

 mu sync.Mutexmu.Lock()defer mu.Unlock()

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

 

  

type smap  {  sync.Mutex  data map[string]}func newSMap() *smap {  return &smap{    data: make(map[),}}func (m *smap) Get(k string)  {  m.Lock()  defer m.Unlock()   m.data[k]}

 

type SMap  {  mu sync.Mutex  data map[}func NewSMap() *SMap {  SMap{    data: make(map[ {  m.mu.Lock()  defer m.mu.Unlock()   m.data[k]}

 

为私有类型或需要实现 Mutex 接口的类型嵌入对于导出的类型,使用私有锁。
复制 Slice 和 Map

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

接收 Slice 和 Map 作为入参

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

BadGood
func (d *Driver) SetTrips(trips []Trip) {  d.trips = trips}trips := ...d1.SetTrips(trips) DID you mean to modify d1.trips?trips[0] = ...

 

func (d * make([]Trip,len(trips))  copy(d.trips,trips)}trips := We can Now modify trips[0] without affecting d1.trips.trips[0] = ...

 

返回 Slice 和 Map

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

BadGood
type Stats  {  sync.Mutex  counters map[int} Snapshot 方法返回当前的状态func (s *Stats) Snapshot() map[ {  s.Lock()  defer s.Unlock()   s.counters} snapshot 不再被锁保护snapshot := stats.Snapshot()

 

type Stats }func (s *Stats) Snapshot() map[ {  s.Lock()  defer s.Unlock()  result := make(map[,len(s.counters))  for k,v := range s.counters {    result[k] = v  }   result} 现在 Snapshot 是一个副本snapshot := stats.Snapshot()

 

Defer 的使用

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

BadGood
p.Lock()if p.count < 10 {  p.Unlock()   p.count}p.count++newCount := p.countp.Unlock() newCount 多个返回语句导致很容易忘记释放锁

 

p.Lock()defer p.Unlock() p.count}p.count++ p.count 更可读

 

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

channel 的大小是 1 或者 None

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

BadGood
 Ought to be enough for anybody!c := make(chan int,64)

 

 size 为 1c := make(chan 1)  或者 非缓冲 channel,size 为 0c := make(chan int)

 

枚举值从 1 开始

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

BadGood
type Operation intconst (  Add Operation = iota  Subtract  Multiply) Add=0,Subtract=1,Multiply=2

 

type Operation  (  Add Operation = iota +   Subtract  Multiply) Add=1,Subtract=2,Multiply=3

 

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

type logoutput  (  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。

BadGood
 package foofunc open() error {  return errors.New(Could not open)} package barfunc use() {  if err := foo.open(); err != nil {    if err.Error() ==  {       handle    } else {      panic(unkNown error)    }  }}

 

 package foovar ErrCouldNotopen = errors.New()func open() error {   ErrCouldNotopen} package bar nil {  if err == foo.ErrCouldNotopen {     handle  }  {    panic()  }}

 

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

BadGood
func open(file ) error {  return fmt.Errorf(file %q not foundif err := open(); err !=if strings.Contains(err.Error(),1)">not found) {      )    }  }}

 

type errNotFound  {  file }func (e errNotFound) Error() return fmt.Sprintf( errNotFound{file: file}}func use() {  if _,ok := err.(errNotFound); ok {      )    }  }}

 

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

type errNotFound bool {  _,ok := err.(errNotFound)   ok}func Open(file  errNotFound{file: file}}if err := foo.Open(foo"); err !=if foo.IsNotFoundError(err) {    )  }}

 

Error 包装

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

如果没有额外增加的上下文并且你想维持原始 error 类型,那么返回原始 error使用 "pkg/errors".Wrap 增加上下文,以至于 error 信息提供更多的上下文,并且 "pkg/errors".Cause 可以用来提取原始 error如果调用者不需要检查或者处理具体的 error 例子,那么使用 fmt.Errorf

推荐去增加上下文信息取代描述模糊的 error,例如 "connection refused",应该返回例如 "Failed to call service foo: connection refused" 这样更有用的 error。

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

处理类型断言失败

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

BadGood
t := i.(string)

 

t,ok := i.()if !ok {   handle the error gracefully}

 

避免 Panic

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

BadGood
func foo(bar ) {  if len(bar) == 0bar must not be empty)  }   ...}func main() {  if len(os.Args) != 2 {    fmt.Println(USAGE: foo <bar>)    os.Exit()  }  foo(os.Args[])}

 

func foo(bar 0     ...   nil}func main() {  if err := foo(os.Args[1]); err != nil {    panic(err)  }}

 

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

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

 

甚至在测试用例中,更偏向于使用 t.Fatal 或者 t.FailNow 解决 panic 确保这个测试被标记为失败。
BadGood
 func TestFoo(t *testing.T)f,err := IoUtil.Tempfile("",1)">if err != nil {  panic(Failed to set up test)}

 

 nil {  t.Fatal()}

 

使用 go.uber.org/atomic

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

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

BadGood
type foo  {  running int32   atomic}func (f* foo) start() {  if atomic.SwAPInt32(&f.running,1)">1) ==  {      already running…       }   start the Foo}func (f *foo) isRunning() return f.running == 1   race!}

 

type foo  {  running atomic.Bool}func (f *foo) start() {  if f.running.Swap(true) {      f.running.Load()}

 

 
性能

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

strconv 优于 fmt

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

BadGood
var i int = ...s := fmt.Sprint(i)

 

 ...s := strconv.Itoa(i)

 

避免 string 到 byte 的转换

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

BadGood
for i := 0; i < b.N; i++ {  w.Write([]byte(Hello world))}

 

data := [] {  w.Write(data)}

 

BenchmarkBad-4   50000000   22.2 ns/op
BenchmarkGood-4  500000000   3.25 ns/op
代码风格聚合相似的声明

Go 支持分组声明。

BadGood
import aimport b"

 

import (  "  )

 

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

BadGood
const a = 1const b = 2var a = var b = type Area float64type Volume float64

 

 (  a =   b = ))type (  Area float64  Volume float64)

 

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

BadGood
type Operation   Subtract  Multiply  ENV_VAR = MY_ENV)

 

type Operation const ENV_VAR = "

 

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

BadGood
func f() var red = color.New(0xff0000)  var green = color.New(0x00ff00var blue = color.New(0x0000ff)  ...}

 

func f()  (    red   = color.New()    green = color.New()    blue  = color.New()  )  ...}

 

包的分组导入的顺序

有两个导入分组:

标准库其他库

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

BadGood
fmtosgo.uber.org/atomicgolang.org/x/sync/errgroup)

 

"  )

 

包命名

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

都是小写字母。没有大写字母或者下划线在大多数场景下没必要重命名包简明扼要。记住,每次调用时都会通过名称来识别。不要复数。例如,要使用 net/url,不要使用 net/urls不要使用 "common","util","shared","lib" 诸如此类的命名。这种方式不太好,无法从名字中获取有效信息。

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

函数命名

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

别名导入

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

net/http  clIEnt example.com/clIEnt-go  trace example.com/trace/v2)

 

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

BadGood
  nettrace golang.net/x/traceruntime/trace  nettrace )

 

函数分组和顺序函数应该按大致的调用顺序排序同一个文件的函数应该按接收者分组

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

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

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

BadGood
func (s *something) Cost() {   calcCost(s.weights)}type something { ... }func calcCost(n int[])  {...}func (s *something) Stop() {...}func newSomething() *something {    something{}}

 

type something { ... }func newSomething() *something{}}func (s * calcCost(s.weights)}func (s *something) Stop() {...}func calcCost(n int {...}

 

减少嵌套

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

BadGood
for _,1)"> range data {  if v.F1 ==  {    v = process(v)    if err := v.Call(); err == nil {      v.Send()    }  err    }  }  {    log.Printf(InvalID v: %vif v.F1 != continue  }    v = process(v)  if err := v.Call(); err != err  }  v.Send()}

 

不必要的 else

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

BadGood
var a int b {  a = 100}  {  a = }

 

a := 10}

 

顶层变量的声明

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

BadGood
var _s string = F()func F() string { return " }

 

var _s = F() F 已经声明了返回一个 string,我们不需要再次指定类型func F() " }

 

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

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

 

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

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

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

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

BadGood
 foo.go (  defaultPort = 8080  defaultUser = user bar.gofunc bar() {  defaultPort := 9090  ...  fmt.Println(Default port 我们将 bar() 的第一行删除,将不会看到编译错误}

 

 (  _defaultPort =   _defaultUser = )

 

结构体的嵌入

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

BadGood
type ClIEnt  {  version   http.ClIEnt}

 

type ClIEnt  {  http.ClIEnt  version }

 

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

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

BadGood
k := User{John",1)">Doetrue}

 

k := User{    Firstname: {}{  op Operation  want }{  {Add,add},{Subtract,1)">subtractvar s = "foo"
s := "foo"

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

BadGood
func f(List []) {  filtered := []{}   range List {    if v >  {      filtered = append(filtered,v)    }  }}

 

func f(List []var filtered []int  if x == ""return []{}}

 

 nil}

 

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

BadGood
func isEmpty(s []return s == nil}

 

func isEmpty(s []return len(s) == }

 

The zero value (a slice declared with var) is usable immediately without make().

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

BadGood
nums := []{} or,nums := make([]int) add1 {  nums = append(nums,1)"> add2 {  nums = append(nums,1)">var nums [])}

 

减少变量的作用域

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

BadGood
err := f.Close() nil {  err}

 

if err := f.Close(); err != err}

 

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

BadGood
if f,err := os.Open(f"); err == nil {  _,err = io.WriteString(f,1)">data err  }   f.Close()}  err}

 

f,1)"> nil {    err}return f.Close()

 

避免裸参数

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

BadGood
 func printInfo(name string,isLocal,done bool)printInfo(true,1)">true)

 

true /* isLocal */,1)"> done */)

 

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

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

 

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

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

BadGood
wantError := unkNown name:\"test\""

 

wantError := `unkNown error:"`

 

初始化结构体

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

BadGood
sval := T{name: } 不一致sptr := (T)sptr.name = bar"

 

sval := T{name: }sptr := &T{name: "}

 

在 Printf 之外格式化字符串

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

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

BadGood
msg := unexpected values %v,%v\nfmt.Printf(msg,1)">1,1)">2)

 

const msg = 2)

 

Printf-style 函数的命名

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

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

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

$ go vet -printfuncs=wrapf,statusf

请参考 go vet: Printf family check。

设计模式表格驱动测试

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

BadGood
 func TestSplitHostPort(t *testing.T)host,port,err := net.SplitHostPort(192.0.2.0:8000)require.NoError(t,err)assert.Equal(t,1)">192.0.2.08000192.0.2.0:httphttp:80001:818tests := []{  give       wantHost   wantPort }{  {    give:      range tests {  t.Run(tt.give,func(t *testing.T) {    host,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 := []}{   range tests {   ...}

 

函数参数可选化

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

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

BadGood
 package dbfunc Connect(  addr *Connection,error) {   timeout 和 caching 必须要提供,哪怕用户想使用默认值db.Connect(addr,db.DefaultTimeout,db.DefaultCaching)db.Connect(addr,newTimeout,false  caching */)db.Connect(addr,1)">*/)

 

type options  {  timeout time.Duration  caching  Option 重写 Connect.type Option  {  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 ) Option {  options) {    o.caching = cache  })} Connect 创建一个 connectionfunc Connect(  addr  options{    timeout: defaultTimeout,caching: defaultCaching,}   range opts {    o.apply(&options)  }   Options 只在需要的时候提供db.Connect(addr)db.Connect(addr,db.WithTimeout(newTimeout))db.Connect(addr,db.WithCaching(false))db.Connect(  addr,db.WithTimeout(newTimeout),)

 

 

请参考,

Self-referential functions and the design of optionsFunctional options for friendly APIs 总结

以上是内存溢出为你收集整理的GO代码风格指南 Uber Go (转载)全部内容,希望文章能够帮你解决GO代码风格指南 Uber Go (转载)所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1252552.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-07
下一篇 2022-06-07

发表评论

登录后才能评论

评论列表(0条)

保存