Golang编码规范之指导原则02
Golang编码规范之指导原则02¶
一 在边界处拷贝 Slices 和 Maps¶
slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。
1.1 接收 Slices 和 Maps¶
请记住,当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。
- Bad
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// 你是要修改 d1.trips 吗?
trips[0] = ...
- Good
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] = ...
1.2 返回 slices 或 maps¶
同样,请注意用户对暴露内部状态的 map 或 slice 的修改。
- Bad
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()
- Good
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()
二 使用 defer 释放资源¶
使用 defer 释放资源,诸如文件和锁。
- Bad
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// 当有多个 return 分支时,很容易遗忘 unlock
- Good
Defer 的开销非常小,只有在您可以证明函数执行时间处于纳秒级的程度时,才应避免这样做。使用 defer 提升可读性是值得的,因为使用它们的成本微不足道。尤其适用于那些不仅仅是简单内存访问的较大的方法,在这些方法中其他计算的资源消耗远超过 defer
。
三 Channel 的 size 要么是 1,要么是无缓冲的¶
channel 通常 size 应为 1 或是无缓冲的。默认情况下,channel 是无缓冲的,其 size 为零。任何其他尺寸都必须经过严格的审查。我们需要考虑如何确定大小,考虑是什么阻止了 channel 在高负载下和阻塞写时的写入,以及当这种情况发生时系统逻辑有哪些变化。(翻译解释:按照原文意思是需要界定通道边界,竞态条件,以及逻辑上下文梳理)
- Bad
- Good
四 枚举从 1 开始¶
在 Go 中引入枚举的标准方法是声明一个自定义类型和一个使用了 iota 的 const 组。由于变量的默认值为 0,因此通常应以非零值开头枚举。
- Bad
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
- Good
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"
包,因为它有助于以更安全、更准确的方式处理这些不正确的假设。
5.1 使用 time.Time
表达瞬时时间¶
在处理时间的瞬间时使用 time.Time
,在比较、添加或减去时间时使用 time.Time
中的方法。
- Bad
- Good
func isActive(now, start, stop time.Time) bool {
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}
5.2 使用 time.Duration
表达时间段¶
在处理时间段时使用 time.Duration
.
- Bad
func poll(delay int) {
for {
// ...
time.Sleep(time.Duration(delay) * time.Millisecond)
}
}
poll(10) // 是几秒钟还是几毫秒?
- Good
回到第一个例子,在一个时间瞬间加上 24 小时,我们用于添加时间的方法取决于意图。如果我们想要下一个日历日 (当前天的下一天) 的同一个时间点,我们应该使用 Time.AddDate
。但是,如果我们想保证某一时刻比前一时刻晚 24 小时,我们应该使用 Time.Add
。
newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)
5.3 对外部系统使用 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
当在这些交互中不能使用 time.Time
时,除非达成一致,否则使用 string
和 RFC 3339 中定义的格式时间戳。默认情况下,Time.UnmarshalText
使用此格式,并可通过 time.RFC3339
在 Time.Format
和 time.Parse
中使用。
尽管这在实践中并不成问题,但请记住,"time"
包不支持解析闰秒时间戳(8728),也不在计算中考虑闰秒(15190)。如果您比较两个时间瞬间,则差异将不包括这两个瞬间之间可能发生的闰秒。