介绍
Uber 是一家美国硅谷的科技公司,也是 Go 语言的早期 adopter。其开源了很多 golang 项目,诸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年末 Uber 将内部的 Go 风格规范 开源到 GitHub,经过一年的积累和更新,该规范已经初具规模,并受到广大 Gopher 的关注。本文是该规范的中文版本。本版本会根据原版实时更新。
本文用于记录对 Uber Go 语言编码规范 的个人理解和学习进度,大家可以通过链接学习最新版本,理解有困难或者没有梯子的小伙伴我的GitHub有更多go相关笔记可以参考我的笔记。每天更新1-3条,欢迎催更。不懂的地方可以评论,大家一起探讨。
目录
对外部系统使用 time.Time 和 time.Duration
指导原则
指向 interface 的指针
您几乎不需要指向接口类型的指针。您应该将接口作为值进行传递,在这样的传递过程中,实质上传递的底层数据仍然可以是指针。
接口实质上在底层用两个字段表示:
- 一个指向某些特定类型信息的指针。您可以将其视为"type"。
- 数据指针。如果存储的数据是指针,则直接存储。如果存储的数据是一个值,则存储指向该值的指针。
官方示例一
如果希望接口方法修改基础数据,则必须使用指针传递 (将对象指针赋值给接口变量)。
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 条是编译器对接口的检查机制, 大体意思是错误使用接口会在编译期报错。 所以可以利用这个机制让部分问题在编译期暴露。
Bad | Good |
---|---|
// 如果 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 的指针基本是不必要的。
Bad | Good |
---|---|
mu := new(sync.Mutex) mu.Lock() | var mu sync.Mutex mu.Lock() |
如果你使用结构体指针,mutex 应该作为结构体的非指针字段。即使该结构体不被导出,也不要直接把 mutex 嵌入到结构体中。
Bad | Good |
---|---|
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 及其方法是 |
在边界处拷贝 Slices 和 Maps
slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。
接收 Slices 和 Maps
请记住,当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。
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] = ... |
Bad情况:
直接赋值切片,只是复制了切片的元数据(指针、长度和容量),而不会复制底层数组。
因此,d1.trips 和 trips 共享同一个底层数组,修改其中一个切片的内容会影响另一个切片。
Good情况:
d1.trips 使用 make
和 copy
创建独立的切片,避免共享数据。
返回 slices 或 maps
同样,请注意用户对暴露内部状态的 map 或 slice 的修改。
Bad | Good |
---|---|
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 释放资源,诸如文件和锁。
Bad | Good |
---|---|
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 在高负载下和阻塞写时的写入,以及当这种情况发生时系统逻辑有哪些变化。(翻译解释:按照原文意思是需要界定通道边界,竞态条件,以及逻辑上下文梳理)
Bad | Good |
---|---|
// 应该足以满足任何情况! c := make(chan int, 64) | // 大小:1 c := make(chan int, 1) // 或者 // 无缓冲 channel,大小为 0 c := make(chan int) |
枚举从 1 开始
在 Go 中引入枚举的标准方法是声明一个自定义类型和一个使用了 iota 的 const 组。由于变量的默认值为 0,因此通常应以非零值开头枚举。
Bad | Good |
---|---|
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 处理时间
时间处理很复杂。关于时间的错误假设通常包括以下几点。
- 一天有 24 小时
- 一小时有 60 分钟
- 一周有七天
- 一年 365 天
- 还有更多
例如,1 表示在一个时间点上加上 24 小时并不总是产生一个新的日历日。
因此,在处理时间时始终使用 "time" 包,它抽象了时间的复杂性,处理了各种边缘情况和异常(如夏令时、闰秒、时区变化),提供了高精度的计算和安全的API,减少了人为错误,确保时间操作的准确性和可靠性。
使用 time.Time
表达瞬时时间
在处理时间的瞬间时使用 time.Time,在比较、添加或减去时间时使用 time.Time
中的方法。
Bad | Good |
---|---|
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 。
Bad | Good |
---|---|
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
例如 :
- Command-line 标志: flag 通过 time.ParseDuration 支持
time.Duration
- JSON: encoding/json 通过其 UnmarshalJSON method 方法支持将
time.Time
编码为 RFC 3339 字符串 - SQL: database/sql 支持将
DATETIME
或TIMESTAMP
列转换为time.Time
,如果底层驱动程序支持则返回 - YAML: gopkg.in/yaml.v2 支持将
time.Time
作为 RFC 3339 字符串,并通过 time.ParseDuration 支持time.Duration
当不能在这些交互中使用 time.Duration
时,请使用 int
或 float64
,并在字段名称中包含单位。
例如,由于 encoding/json
不支持 time.Duration
,因此该单位包含在字段的名称中。
Bad | Good |
---|---|
// {"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)。如果您比较两个时间瞬间,则差异将不包括这两个瞬间之间可能发生的闰秒。