Golang编码规范基础指导原则
使用 defer 释放资源
使用 defer 释放资源,诸如文件和锁。
不推荐写法:
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
。
Channel 的 size 要么是 1,要么是无缓冲的
channel 通常 size 应为 1 或是无缓冲的。默认情况下,channel 是无缓冲的,其 size 为零。任何其他尺寸都必须经过严格的审查。我们需要考虑如何确定大小,考虑是什么阻止了 channel 在高负载下和阻塞写时的写入,以及当这种情况发生时系统逻辑有哪些变化。(翻译解释:按照原文意思是需要界定通道边界,竞态条件,以及逻辑上下文梳理)
不推荐写法:
// 应该足以满足任何情况!
c := make(chan int, 64)
推荐写法:
// 大小:1
c := make(chan int, 1) // 或者
// 无缓冲 channel,大小为 0
c := make(chan int)
枚举从 1 开始
在 Go 中引入枚举的标准方法是声明一个自定义类型和一个使用了 iota 的 const 组。由于变量的默认值为 0,因此通常应以非零值开头枚举。
不推荐写法:
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
使用atomic
使用 sync/atomic 包的原子操作对原始类型 (int32
, int64
等)进行操作,因为很容易忘记使用原子操作来读取或修改变量。
go.uber.org/atomic 通过隐藏基础类型为这些操作增加了类型安全性。此外,它包括一个方便的atomic.Bool
类型。
不推荐写法:
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!
}
推荐写法:
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()
}
避免可变全局变量
使用选择依赖注入方式避免改变全局变量。 既适用于函数指针又适用于其他值类型
不推荐写法1:
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
推荐写法1:
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
不推荐写法2:
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
}
推荐写法2:
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}
避免使用内置名称
Go 语言规范 概述了几个内置的, 不应在 Go 项目中使用的 预先声明的标识符。
根据上下文的不同,将这些标识符作为名称重复使用, 将在当前作用域(或任何嵌套作用域)中隐藏原始标识符,或者混淆代码。 在最好的情况下,编译器会报错;在最坏的情况下,这样的代码可能会引入潜在的、难以恢复的错误。
不推荐写法1:
var error string
// `error` 作用域隐式覆盖
// or
func handleErrorMessage(error string) {
// `error` 作用域隐式覆盖
}
推荐写法1:
var errorMessage string
// `error` 指向内置的非覆盖
// or
func handleErrorMessage(msg string) {
// `error` 指向内置的非覆盖
}
不推荐写法2:
type Foo struct {
// 虽然这些字段在技术上不构成阴影,但`error`或`string`字符串的重映射现在是不明确的。
error error
string string
}
func (f Foo) Error() error {
// `error` 和 `f.error` 在视觉上是相似的
return f.error
}
func (f Foo) String() string {
// `string` and `f.string` 在视觉上是相似的
return f.string
}
推荐写法2:
type Foo struct {
// `error` and `string` 现在是明确的。
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
注意,编译器在使用预先分隔的标识符时不会生成错误, 但是诸如go vet
之类的工具会正确地指出这些和其他情况下的隐式问题。
避免使用 init()
尽可能避免使用init()
。当init()
是不可避免或可取的,代码应先尝试:
- 无论程序环境或调用如何,都要完全确定。
- 避免依赖于其他
init()
函数的顺序或副作用。虽然init()
顺序是明确的,但代码可以更改, 因此init()
函数之间的关系可能会使代码变得脆弱和容易出错。 - 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录、程序参数/输入等。
- 避免
I/O
,包括文件系统、网络和系统调用。
不能满足这些要求的代码可能属于要作为main()
调用的一部分(或程序生命周期中的其他地方), 或者作为main()
本身的一部分写入。特别是,打算由其他程序使用的库应该特别注意完全确定性, 而不是执行“init magic”
不推荐写法1:
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
推荐写法1:
var _defaultFoo = Foo{
// ...
}
// or,为了更好的可测试性:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
不推荐写法2:
type Config struct {
// ...
}
var _config Config
func init() {
// Bad: 基于当前目录
cwd, _ := os.Getwd()
// Bad: I/O
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
推荐写法2:
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// handle err
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// handle err
var config Config
yaml.Unmarshal(raw, &config)
return config
}
考虑到上述情况,在某些情况下,init()
可能更可取或是必要的,可能包括:
- 不能表示为单个赋值的复杂表达式。
- 可插入的钩子,如
database/sql
、编码类型注册表等。
追加时优先指定切片容量
追加时优先指定切片容量
在尽可能的情况下,在初始化要追加的切片时为make()
提供一个容量值。
不推荐写法:
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
推荐写法:
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
在序列化结构中使用字段标记
任何序列化到JSON、YAML、, 或其他支持基于标记的字段命名的格式应使用相关标记进行注释。
不推荐写法:
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
推荐写法:
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Safe to rename Name to Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
理论上: 结构的序列化形式是不同系统之间的契约。 对序列化表单结构(包括字段名)的更改会破坏此约定。在标记中指定字段名使约定明确, 它还可以通过重构或重命名字段来防止意外违反约定。