文章目录

  • 引言
  • 准则
    • Pointers to Interfaces
    • 接收器和接口
    • 零值互斥是有效的
    • 在边界处复制Slices和Maps
      • 作为参数接收Slices和Maps
      • 返回时Slices 和 Maps
    • 用defer处理清理工作
    • Channel的通道大小为1 或 无缓冲
    • 从1 开始枚举
    • 错误类型
    • 错误包装
    • 处理类型断言失败处理类型断言失败
    • 不要使用panic
    • go.uber.org/atomic
  • 性能
    • 优先使用strconv而不是fmt
    • 避免string到[]byte的转换
  • 风格
    • 组相似声明
    • import 顺序
    • 包命名
    • 函数命名
    • 导入别名
    • 函数分组排序
    • 减少嵌套
    • 不必要的`else`
    • 顶级变量声明
    • 使用`_`前缀未导出的全局变量
    • 嵌入结构
    • 使用字段名称初始化结构
    • 局部变量声明
    • `nil`是有效切片
    • 缩小变量范围
    • 初始化结构引用

引言

样式是支配我们代码的惯例。 术语“样式”有点用词不当,因为这些约定不仅仅涵盖那些可以由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工具的编辑器支持中找到信息:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

准则

Pointers to Interfaces

您几乎不需要指向接口的指针。 您应该将接口作为值传递-底层数据仍然可以是指针。

一个接口是两个字段:

  • 指向某些特定类型信息的指针。 您可以将其视为“类型”。
  • 数据指针。 如果存储的数据是指针,则直接存储。 如果存储的数据是一个值,则存储指向该值的指针。

如果要接口方法修改基础数据,则必须使用指针。

接收器和接口

具有值接收器的方法可以在指针和值上调用。
例如

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

同样,即使该方法具有值接收器,也可以通过指针来满足接口。

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 has a good write up on Pointers vs. Values.

零值互斥是有效的

sync.Mutexsync.RWMutex的零值是有效的,因此不需要指向互斥量的指针。

  • 推荐使用:
var mu sync.Mutex
mu.Lock()
  • 不推荐使用:
mu := new(sync.Mutex)
mu.Lock()

如果通过指针使用结构,则互斥体可以是非指针字段,或者最好直接嵌入到该结构中。

  • 为专用类型或需要实现Mutex接口的类型嵌入。
type smap struct {sync.Mutexdata 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.Mutexdata 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]
}

在边界处复制Slices和Maps

Slices和Maps包含指向基础数据的指针,因此在需要复制它们时要特别注意方案。

作为参数接收Slices和Maps

  • 推荐
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] = ...
  • 不推荐
func (d *Driver) SetTrips(trips []Trip) {d.trips = trips
}trips := ...
d1.SetTrips(trips)// Did you mean to modify d1.trips?
trips[0] = ...

返回时Slices 和 Maps

同样,请注意用户对显示内部状态的map或切片的修改。

  • 推荐
type Stats struct {sync.Mutexcounters 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()
  • 不推荐
type Stats struct {sync.Mutexcounters 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()

用defer处理清理工作

使用defer清理资源,例如文件和锁。

  • 推荐
p.Lock()
defer p.Unlock()if p.count < 10 {return p.count
}p.count++
return p.count// more readable
  • 不推荐
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

Defer的开销非常小,只有在您可以证明函数执行时间处于纳秒级的程度时,才应避免这样做。 比起微不足道的成本,使用defer的可读性是值得的。 对于具有比简单的内存访问更多的更大的方法尤其如此,其他方法的计算比defer要重要得多。

Channel的通道大小为1 或 无缓冲

通道通常应为1大小或无缓冲。 默认情况下,通道是无缓冲的,大小为零。 任何其他大小都必须经过严格的审查。 考虑如何确定大小,什么阻塞通道在负载下填满并阻塞写入器,以及发生这种情况时会发生什么。

  • 推荐
// Size of one
c := make(chan int, 1) // or
// Unbuffered channel, size of zero
c := make(chan int)
  • 不推荐
// Ought to be enough for anybody!
c := make(chan int, 64)

从1 开始枚举

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

  • 推荐
type Operation intconst (Add Operation = iota + 1SubtractMultiply
)// Add=1, Subtract=2, Multiply=3
  • 不推荐
type Operation intconst (Add Operation = iotaSubtractMultiply
)// Add=0, Subtract=1, Multiply=2

在某些情况下,使用零值是有意义的,例如,当零值是理想的默认行为时。

type LogOutput intconst (LogToStdout LogOutput = iotaLogToFileLogToRemote
)// LogToStdout=0, LogToFile=1, LogToRemote=2

错误类型

有多种声明错误的选项:

  • errors.New对于简单静态字符串的错误
  • fmt.Errorf用于格式化的错误字符串
  • 实现Error()方法的自定义类型
  • 包装错误使用"pkg/errors".Wrap
    返回错误时,请考虑以下因素以确定最佳选择:
  • 这是一个不需要额外信息的简单错误吗?如果是,errors.New 可以满足
  • 客户需要检测并处理此错误吗? 如果是这样,则应使用自定义类型,并实现Error()方法。
  • 您是否正在传播下游函数返回的错误?如果是,查看section on error wrapping.
  • 否则, fmt.Errorf就ok。

如果客户端需要检测错误,并且您已经使用errors创建了一个简单的错误,请使用var。

  • 推荐
// package foovar ErrCouldNotOpen = errors.New("could not open")func Open() error {return ErrCouldNotOpen
}// package barif err := foo.Open(); err != nil {if err == foo.ErrCouldNotOpen {// handle} else {panic("unknown error")}
}
  • 不推荐
// package foofunc Open() error {return errors.New("could not open")
}// package barfunc use() {if err := foo.Open(); err != nil {if err.Error() == "could not open" {// 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")}}
}
  • 不推荐
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")}}
}

直接导出自定义错误类型时要小心,因为它们已成为程序包公共API的一部分。 最好公开匹配器功能以检查错误。

// package footype 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 barif err := foo.Open("foo"); err != nil {if foo.IsNotFoundError(err) {// handle} else {panic("unknown error")}
}

错误包装

调用失败时,有三种主要的错误传播方式:

  • 如果没有要添加的其他上下文,并且您想要维护原始错误类型,则返回原始错误。
  • 使用pkg / errors增加上下文。包装以便错误消息提供更多上下而且pkg / errors.Cause可用于提取原始错误。
  • 如果调用方不需要检测或处理该特定错误情况使用fmt.Errorf
    使用示例:
  • 推荐
s, err := store.New()
if err != nil {return fmt.Errorf("new store: %s", err)
}
//x: y: new store: the error
  • 不推荐
s, err := store.New()
if err != nil {return fmt.Errorf("failed to create new store: %s", err)
}
//failed to x: failed to y: failed to create new store: the error

但是,一旦将错误发送到另一个系统,就应该清楚该消息是一个错误(例如,日志中的err标记或“Failed”前缀)。

处理类型断言失败处理类型断言失败

类型断言的单个返回值形式对不正确的类型将会panic。 因此,请始终使用“,ok”的习惯用法。

  • 推荐使用
t, ok := i.(string)
if !ok {// handle the error gracefully
}
  • 不推荐单个返回值:
t := i.(string)

不要使用panic

在生产中运行的代码必须避免出现panic情况。panic 是级联失败的主要根源。 如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它。

  • 推荐
func foo(bar string) error {if len(bar) == 0return 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)}
}
  • 不推荐
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])
}

Panic/recover不是错误处理策略。仅当发生不可恢复的事情(例如取消nil引用)时,程序才必须panic。程序初始化是一个例外:程序启动时应使程序中止的不良情况可能会引起panic

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

即使在测试中,也要优先选择t.Fatalt.FailNow来代替panic,以确保测试标记为失败。

  • 推荐
// func TestFoo(t *testing.T)f, err := ioutil.TempFile("", "test")
if err != nil {t.Fatal("failed to set up test")
}
  • 不推荐
// func TestFoo(t *testing.T)f, err := ioutil.TempFile("", "test")
if err != nil {panic("failed to set up test")
}

go.uber.org/atomic

使用sync / atomic包的原子操作对原始类型(int32,int64等)进行操作,因此很容易忘记使用原子操作来读取或修改变量。

go.uber.org/atomic通过隐藏基础类型为这些操作增加了类型安全性。 另外,它包括一个方便的atomic.Bool类型。

  • 推荐
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()
}
  • 不推荐官方:
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!
}

性能

优先使用strconv而不是fmt

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

避免string到[]byte的转换

不要重复从固定字符串创建字节片。相反,请执行一次转换并捕获结果。

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

风格

组相似声明

  • 推荐
import ("a""b"
)const (a = 1b = 2
)var (a = 1b = 2
)type (Area float64Volume float64
)
  • 不推荐
import "a"
import "b"const a = 1
const b = 2var a = 1
var b = 2type Area float64
type Volume float64

仅与组相关的声明。 不要对不相关的声明进行分组。

  • 推荐
type Operation intconst (Add Operation = iota + 1SubtractMultiply
)const ENV_VAR = "MY_ENV"
  • 不推荐
type Operation intconst (Add Operation = iota + 1SubtractMultiplyENV_VAR = "MY_ENV"
)

组不受使用位置的限制。
例如,您可以在函数内部使用它们。

func f() string {var (red   = color.New(0xff0000)green = color.New(0x00ff00)blue  = color.New(0x0000ff))...
}

import 顺序

应该有两个导入组:

  • 标准库
  • 其他一切
    默认情况下,这是goimports应用的分组。
import ("fmt""os""go.uber.org/atomic""golang.org/x/sync/errgroup"
)

包命名

命名包时,按如下原则选择:

  • 全部小写。没有大写或下划线
  • 大多数情况不需要在导入时重命名
  • 简短而简洁。请记住,在每个调用站点上都完整标识了该名称。
  • 不复数。例如,net / url,而不是net / url。
  • 不用commonutilsharedlib。这些是不好的,无用的名称。

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

函数命名

导入别名

函数分组排序

  • 函数应按粗略的调用顺序排序。
  • 文件中的函数应按接收者分组。
    因此,导出的函数应首先出现在文件中,然后是structconstvar定义。

减少嵌套

代码应通过尽可能先处理错误情况/特殊情况并尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码量。

不必要的else

顶级变量声明

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

使用_前缀未导出的全局变量

嵌入结构

type Client struct {http.Clientversion int
}

使用字段名称初始化结构

## suggest
k := User{FirstName: "John",LastName: "Doe",Admin: true,
}
## no suggest
k := User{"John", "Doe", true}
  • 例外: 如果字段数少于或等于3,则测试表中的字段名可以省略。

局部变量声明

如果将变量显式设置为某个值,则应使用短变量声明:=

  • 例外:但是,在某些情况下,使用var关键字时默认值会更清晰。例如,声明为空片。

nil是有效切片

nil是长度为0的有效切片。

缩小变量范围

# suggest
if err := ioutil.WriteFile(name, data, 0644); err != nil {return err
}# no suggest
err := ioutil.WriteFile(name, data, 0644)
if err != nil {return err
}

初始化结构引用

初始化结构引用时,请使用&T{}而不是new(T),以使其与结构初始化一致。

Uber 公司Golang编程规范【翻译】相关推荐

  1. 一公司C#编程规范v2.0(转)

     C#编程规范 Version 2.0 目录 第一章 概述      4 规范制定原则      4 术语定义      4 Pascal 大小写      4 Camel 大小写      4 文件 ...

  2. Uber Go 语言编程规范

    相信很多人前两天都看到 Uber 在 github 上面开源的 Go 语言编程规范了,原文在这里:https://github.com/uber-go/guide/blob/master/style. ...

  3. sun公司java编程规范【转载】

    java编程规范要学习的大致内容有如下部分,一个目录: Java编码规范 1 1.说明 3 1.1为什么要有编码规范 3 1.2版权声明 3 2.文件名(File Names) 3 2.1文件后缀(F ...

  4. Uber 公司推出的 GoLang 编程规范

    参考: Go-design-codes/guide.md at main · yigenshutiao/Go-design-codes · GitHubSome cool code written b ...

  5. 华为公司软件编程规范

    目  录 1 排版 6 2 注释 11 3 标识符命名 18 4 可读性 20 5 变量.结构 22 6 函数.过程 28 7 可测性 36 8 程序效率 40 9 质量保证 44 10 代码编辑.编 ...

  6. Uber Go 语言编程规范:使用 go.uber.org/atomic

    通过sync/atomic 包的原子操作对原始类型s(int32, int64, etc.) 进行操作的时候,很容易忘记在对变量进行读取和修改的时候,使用原子操作. 而go.uber.org/atom ...

  7. Uber Go 语言编程规范:避免语义不明确的参数(Naked Parameters)

    函数调用中的意义不明确的参数(Naked parameters )可能会影响可读性,当参数名称的含义不明显时,请为参数添加 C 样式注释 (/* ... */) Bad // func printIn ...

  8. 公司软件系统编程格式规范

    根据网络资源整理的公司软件系统编程规范,各开发人员予以执行此规范. 1.  前言 为确保系统源程序可读性,从而增强系统可维护性,兹制定下述编程规范,以规范系统各部分编程.系统继承的其它资源中的源程序也 ...

  9. 最近想学习一下编译原理,做一个编程规范的检测工具

    因为公司对编程规范要求的很严格,然而很多人却都没有规范的习惯,所以很多问题还是存在于代码中.有一次机会想做一个编程规范检测的工具,可惜自己能力有限,很多东西都不会.偶然想到了编译原理,可惜我没学过.拿 ...

最新文章

  1. The type List is not generic
  2. PHP框架的ORM思想:O类的实例化 R数据表 M映射XML
  3. [转]C++中的static关键字的总结
  4. feign如何使用?
  5. windows上使用的免费连接linux终端xshell6,xftp6下载
  6. pdf保存如何带批注_带有批注的SpringSelenium测试
  7. 如何用耳机翻页_游戏耳机的经典之作—罗技(G)Astro A40体验
  8. Server操作Mxd文件详细讲解
  9. 设置TextField内文字距左边框的距离
  10. git修改已提交记录的注释
  11. android ffmpeg编译so,Android FFmpeg学习(一),将FFmpeg编译成so文件
  12. Android学习系列(16)--App列表之圆角ListView
  13. 一车abs线路怎么量_神木沥青拌合站烧火油怎么购买更划算
  14. MySQL延时更改数据_mysql数据库备份设置延时备份方法(mysql主从配置)
  15. 基于阿尔法贝塔剪枝算法的五子棋_C4.5算法剪枝2
  16. 网站扫描服务器全部开放端口,服务器开放端口扫描
  17. 30 岁的超级玛丽怎样改变了游戏行业?
  18. CSS命名与书写规范
  19. 使用Foxmail登录阿里企业邮箱(钉钉邮箱)
  20. 兼容IE,Firefox,chrome等浏览器 : 设为首页和收藏的Javascript代码

热门文章

  1. 迅如疾风 PHPWind 6.3.2 测试手记
  2. Nginx高阶用法(一)
  3. Linux命令 - mv命令
  4. 科技是一把双刃剑-医学技术
  5. 同样是人,不可能一天就能修炼成功!
  6. 实验1动态规划——小明打王者
  7. 计算机应用助理工程师证书查询,有助理工程师的证书吗?怎么查询助理工程师职称?...
  8. cocos2d-x 3.6版连连看加载资源
  9. 突发!阿里组织架构大调整
  10. c 语言ktv项目,KTV夜总会包厢里常玩的几种游戏,当然还有更多更好的娱乐项目,仅供参考...