Uber Go 语言编码规范

介绍

Uber 是一家美国硅谷的科技公司,也是 Go 语言的早期 adopter。其开源了很多 golang 项目,诸如被 Gopher 圈熟知的 zapjaeger 等。2018 年年末 Uber 将内部的 Go 风格规范 开源到 GitHub,经过一年的积累和更新,该规范已经初具规模,并受到广大 Gopher 的关注。本文是该规范的中文版本。本版本会根据原版实时更新。

github原文地址

本文用于记录对 Uber Go 语言编码规范 的个人理解和学习进度,大家可以通过链接学习最新版本,理解有困难或者没有梯子的小伙伴我的GitHub有更多go相关笔记可以参考我的笔记。每天更新1-3条,欢迎催更。不懂的地方可以评论,大家一起探讨。

我的GitHub有更多go相关笔记


目录

介绍

指导原则

指向 interface 的指针

官方示例一

关键区别总结

为什么需要指针接收者?

 常见误区

 官方示例二

 错误原因

接口本身是引用类型

Interface 合理性验证

 导出类型的检查

非导出类型的检查

接口变更同步

接收器 (receiver) 与接口

零值 Mutex 是有效的

在边界处拷贝 Slices 和 Maps

接收 Slices 和 Maps

返回 slices 或 maps

使用 defer 释放资源

Channel 大小应为 1 或是无缓冲的

枚举从 1 开始

使用 time 处理时间

使用 time.Time 表达瞬时时间

使用 time.Duration 表达时间段

对外部系统使用 time.Time 和 time.Duration


指导原则

指向 interface 的指针

您几乎不需要指向接口类型的指针。您应该将接口作为值进行传递,在这样的传递过程中,实质上传递的底层数据仍然可以是指针。

接口实质上在底层用两个字段表示:

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

官方示例一

如果希望接口方法修改基础数据,则必须使用指针传递 (将对象指针赋值给接口变量)。

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

// f1.f() 无法修改底层数据
// f2.f() 可以修改底层数据,给接口变量 f2 赋值时使用的是对象指针
var f1 F = S1{}
var f2 F = &S2{}
关键区别总结
特性S1(值接收者)S2(指针接收者)
接口实现者S1 和 *S1仅 *S2
赋值方式可用值或指针(如 S1{} 或 &S1{}必须用指针(如 &S2{}
方法调用行为操作副本,无法修改原数据操作原始数据,可修改
为什么需要指针接收者?
  • 修改数据:若方法需要修改结构体内部状态,必须使用指针接收者。

  • 避免拷贝:大型结构体使用指针接收者可以避免值拷贝的开销。

 常见误区
  • 值接收者 + 指针调用:Go 允许 (&S1{}).f(),但方法内部仍操作副本。

  • 指针接收者 + 值调用S2{}.f() 会编译错误,因为值无法隐式转换为指针。

我的示例:

package no1

import "fmt"

type F interface {
	f()
}

type S1 struct {
	name string
}

func (s S1) f() {
	s.name = "f"
	fmt.Println(s.name)
}

type S2 struct {
	name string
}

func (s *S2) f() {
	s.name = "f"
	fmt.Println(s.name)
}

func Test() {
	var f1 F = S1{name: "f1"}
	var f2 F = &S1{name: "f2"}
	var f3 F = &S2{name: "f3"}
	f1.f()
	f2.f()
	f3.f()
	fmt.Println(f1, f2, f3)
}
输出:
f
f
f
{f1} &{f2} &{f}

由此证明关于值接收者与指针接收者的相关分析正确。

 官方示例二

永远不要使用指向interface的指针,这个是没有意义的。在go语言中,接口本身就是引用类型,换句话说,接口类型本身就是一个指针。对于我的需求,其实test的参数只要是myinterface就可以了,只需要在传值的时候,传*mystruct类型(也只能传*mystruct类型)。

type myinterface interface{
	print()
}
func test(value *myinterface){
	//someting to do ...
}

type mystruct struct {
	i int
}
//实现接口
func (this *mystruct) print(){
	fmt.Println(this.i)
	this.i=1
}
func main(){
m := &mystruct{0}
test(m)//错误
test(*m)//错误
}
 错误原因

test 函数参数错误

  • 接口类型本身是引用类型,不需要用指针(*myinterface
  • 使用 *myinterface 会导致需要传递「指向接口变量的指针」,而不是直接传递实现了接口的对象

调用时的类型问题

  • m 是 *mystruct 类型,但 test 需要 *myinterface 类型,二者不兼容
  • *m 是 mystruct 类型,但 mystruct 没有实现接口(因为 print 方法是指针接收者)
接口本身是引用类型

接口变量底层结构:

var value myinterface = &mystruct{0}

内存结构:

类型指针 (type)指向 *mystruct 的类型信息
数据指针 (data)指向实际的 &mystruct{0} 对象

接口变量本身已经包含指针,传递接口变量时相当于传递一个「包含指针的结构体」,不需要再用 *myinterface 去包装它。

Interface 合理性验证

在编译时验证接口的符合性。这包括:

  • 将实现特定接口的导出类型作为接口 API 的一部分进行检查
  • 实现同一接口的 (导出和非导出) 类型属于实现类型的集合
  • 任何违反接口合理性检查的场景,都会终止编译,并通知给用户

补充:上面 3 条是编译器对接口的检查机制, 大体意思是错误使用接口会在编译期报错。 所以可以利用这个机制让部分问题在编译期暴露。

BadGood
// 如果 Handler 没有实现 http.Handler,会在运行时报错
type Handler struct {
  // ...
}
func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  ...
}
type Handler struct {
  // ...
}
// 用于触发编译期的接口的合理性检查机制
// 如果 Handler 没有实现 http.Handler,会在编译期报错
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

如果 *Handler 与 http.Handler 的接口不匹配, 那么语句 var _ http.Handler = (*Handler)(nil) 将无法编译通过。

赋值的右边应该是断言类型的零值。 对于指针类型(如 *Handler)、切片和映射,这是 nil; 对于结构类型,这是空结构。

type LogHandler struct {
  h   http.Handler
  log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

通过 var _ Interface = Type{} 或 var _ Interface = (*Type)(nil) 触发编译器检查。

 导出类型的检查

// 导出结构体必须显式检查接口实现
type PublicHandler struct{}

// 正确:指针接收者实现接口方法
func (h *PublicHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {}

// 编译时检查(指针类型)
var _ http.Handler = (*PublicHandler)(nil) // 通过

导出类型:即可以在包外部调用的类型。

非导出类型的检查

// 内部使用的非导出类型也需验证
type internalHandler struct{}

func (h *internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {}

// 包内编译检查
var _ http.Handler = (*internalHandler)(nil) // 通过

非导出类型:即不可以在包外部调用的类型。

未检查的风险:可能被可导出方法/函数调用,造成运行时panic

func NewHandler() http.Handler {
    return &internalHandler{} // 若方法未实现,运行时赋值触发 panic
}

接口变更同步

当接口新增方法时,编译器通过检查语句提示实现缺失:

// 原接口
type Writer interface { 
    Write([]byte)
}

// 新增方法后的接口
type Writer interface { 
    Write([]byte) 
    Flush() error  // 新增方法
}

// 原有类型未实现 Flush 时,检查语句触发编译错误
var _ Writer = (*MyWriter)(nil) 

接收器 (receiver) 与接口

使用值接收器的方法既可以通过值调用,也可以通过指针调用。

带指针接收器的方法只能通过指针或 addressable values 调用。

例如,

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[1].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 是一个值,而 S2 的 f 方法中没有使用值接收器
//   i = s2Val

Effective Go 中有一段关于 pointers vs. values 的精彩讲解。

这部分与指向 interface 的指针的补充内容重合,不理解的可以回去看看那个部分。

零值 Mutex 是有效的

零值 sync.Mutex 和 sync.RWMutex 是有效的。这意味着你可以直接声明一个 Mutex 变量并使用它,而不需要显式地初始化它。所以指向 mutex 的指针基本是不必要的

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

如果你使用结构体指针,mutex 应该作为结构体的非指针字段。即使该结构体不被导出,也不要直接把 mutex 嵌入到结构体中

BadGood
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 字段, Lock 和 Unlock 方法是 SMap 导出的 API 中不刻意说明的一部分。

mutex 及其方法是 SMap 的实现细节,对其调用者不可见

在边界处拷贝 Slices 和 Maps

slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。

接收 Slices 和 Maps

请记住,当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。

BadGood
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] = ...

Bad情况:

直接赋值切片,只是复制了切片的元数据(指针、长度和容量),而不会复制底层数组。

因此,d1.trips 和 trips 共享同一个底层数组,修改其中一个切片的内容会影响另一个切片。

Good情况:

d1.trips 使用 make 和 copy 创建独立的切片,避免共享数据。

返回 slices 或 maps

同样,请注意用户对暴露内部状态的 map 或 slice 的修改。

BadGood
type Stats struct {
  mu sync.Mutex

  counters map[string]int
}

// Snapshot 返回当前状态。
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  return s.counters
}

// snapshot 不再受互斥锁保护
// 因此对 snapshot 的任何访问都将受到数据竞争的影响
// 影响 stats.counters
snapshot := stats.Snapshot()
type Stats struct {
  mu sync.Mutex

  counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  result := make(map[string]int, len(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}

// snapshot 现在是一个拷贝
snapshot := stats.Snapshot()

Bad情况:

直接返回 map 或 slice 的引用,会导致外部代码可以绕过锁直接修改内部数据,引发数据竞争或意外的副作用。

Good情况:

返回深拷贝的副本,通过创建一个新的 map 或 slice 并复制数据,可以确保外部代码无法修改内部状态。

使用 defer 释放资源

使用 defer 释放资源,诸如文件和锁。

BadGood
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// 当有多个 return 分支时,很容易遗忘 unlock
p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// 更可读

Defer 的开销非常小,只有在您可以证明函数执行时间处于纳秒级的程度时,才应避免这样做。使用 defer 提升可读性是值得的,因为使用它们的成本微不足道。尤其适用于那些不仅仅是简单内存访问的较大的方法,在这些方法中其他计算的资源消耗远超过 defer

Bad情况:

手动管理锁的释放,在返回值之前必须释放锁。

如果在解锁后直接返回 p.count,其他 goroutine 可能会在解锁后立即修改 p.count,导致返回的值不一致。

使用 newCount 可以明确表示返回的值是在锁保护下的快照,而不是解锁后的实时值。

Good情况:

直接返回 p.count 是安全的,因为 defer p.Unlock() 确保了在函数返回时解锁,而 p.count 的值是在锁的保护下读取的。

Channel 大小应为 1 或是无缓冲的

channel 通常 size 应为 1 或是无缓冲的。默认情况下,channel 是无缓冲的,其 size 为零。任何其他尺寸都必须经过严格的审查。我们需要考虑如何确定大小,考虑是什么阻止了 channel 在高负载下和阻塞写时的写入,以及当这种情况发生时系统逻辑有哪些变化。(翻译解释:按照原文意思是需要界定通道边界,竞态条件,以及逻辑上下文梳理)

BadGood
// 应该足以满足任何情况!
c := make(chan int, 64)
// 大小:1
c := make(chan int, 1) // 或者
// 无缓冲 channel,大小为 0
c := make(chan int)

枚举从 1 开始

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

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

使用 time 处理时间

时间处理很复杂。关于时间的错误假设通常包括以下几点。

  1. 一天有 24 小时
  2. 一小时有 60 分钟
  3. 一周有七天
  4. 一年 365 天
  5. 还有更多

例如,1 表示在一个时间点上加上 24 小时并不总是产生一个新的日历日。

因此,在处理时间时始终使用 "time" 包,它抽象了时间的复杂性,处理了各种边缘情况和异常(如夏令时、闰秒、时区变化),提供了高精度的计算和安全的API,减少了人为错误,确保时间操作的准确性和可靠性。

使用 time.Time 表达瞬时时间

在处理时间的瞬间时使用 time.Time,在比较、添加或减去时间时使用 time.Time 中的方法。

BadGood
func isActive(now, start, stop int) bool {
  return start <= now && now < stop
}
func isActive(now, start, stop time.Time) bool {
  return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}

使用 time.Duration 表达时间段

在处理时间段时使用 time.Duration 。

BadGood
func poll(delay int) {
  for {
    // ...
    time.Sleep(time.Duration(delay) * time.Millisecond)
  }
}
poll(10) // 是几秒钟还是几毫秒?
func poll(delay time.Duration) {
  for {
    // ...
    time.Sleep(delay)
  }
}
poll(10*time.Second)

回到第一个例子,在一个时间瞬间加上 24 小时,我们用于添加时间的方法取决于意图。

如果我们想要下一个日历日 (当前天的下一天) 的同一个时间点,我们应该使用 Time.AddDate。但是,如果我们想保证某一时刻比前一时刻晚 24 小时,我们应该使用 Time.Add

newDay := t.AddDate(0 , 0 , 1 )  // 三个参数分别代表年,月,日
maybeNewDay := t.Add(24 * time.Hour)

对外部系统使用 time.Time 和 time.Duration

尽可能在与外部系统的交互中使用 time.Duration 和 time.Time 例如 :

当不能在这些交互中使用 time.Duration 时,请使用 int 或 float64,并在字段名称中包含单位

例如,由于 encoding/json 不支持 time.Duration,因此该单位包含在字段的名称中。

BadGood
// {"interval": 2}
type Config struct {
  Interval int `json:"interval"`
}
// {"intervalMillis": 2000} 明确单位为毫秒
type Config struct {
  IntervalMillis int `json:"intervalMillis"`
}

当在这些交互中不能使用 time.Time 时,除非达成一致,否则使用 string 和 RFC 3339 中定义的格式时间戳。默认情况下,Time.UnmarshalText 使用此格式,并可通过 time.RFC3339 在 Time.Format 和 time.Parse 中使用。

尽管这在实践中并不成问题,但请记住,time 包不支持解析闰秒时间戳8728),也不在计算中考虑闰秒(15190)。如果您比较两个时间瞬间,则差异将不包括这两个瞬间之间可能发生的闰秒。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值