Go:惯用高效编码参考
目录
1. 错误处理 {#errors}
// ❌ 忽略错误
result, _ := os.Open(path)
// ✅ — 始终处理错误;只有当错误明显无关紧要时才使用 _
result, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
// ❌ 冗余的错误变量
var err1 error
err1 = doA()
if err1 != nil { return err1 }
err1 = doB()
if err1 != nil { return err1 }
// ✅ — 每次使用 := 或 = 都是可以的;这是地道的 Go 写法。不要试图“修复”它。
// 你可以简化的是:当 if 体只有单个 return 时,可以写成一行
if err := doA(); err != nil { return err }
if err := doB(); err != nil { return err }
// ❌ 无附加值的自定义错误类型
type MyError struct{ msg string }
func (e MyError) Error() string { return e.msg }
// ✅ — 使用 errors.New 或 fmt.Errorf,除非调用者需要检查类型
var ErrNotFound = errors.New("not found")
return fmt.Errorf("lookup %q: %w", key, ErrNotFound)
使用 %w(而非 %v)包装错误,以便调用者可以使用 errors.Is / errors.As。
2. 切片与映射 {#slices}
// ❌ 在已知大小时不预先分配切片容量
var result []string
for _, item := range items {
result = append(result, item.Name)
}
// ✅
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, item.Name)
}
// ❌ 向映射写入前手动检查键是否存在
if _, ok := m[key]; !ok {
m[key] = []string{}
}
m[key] = append(m[key], value)
// ✅ — 向 nil 切片追加在 Go 中是合法的
m[key] = append(m[key], value)
// ❌ 通过赋值复制映射(只复制了引用)
copy := original
// ✅
copy := make(map[K]V, len(original))
for k, v := range original { copy[k] = v }
3. 协程与通道 {#concurrency}
// ❌ 无生命周期的乱跑协程
go doWork()
// ✅ — 使用 errgroup 或 WaitGroup 来跟踪完成状态
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
// ❌ 无缓冲通道导致不必要的协程阻塞
ch := make(chan Result)
go func() { ch <- compute() }()
result := <-ch
// ✅ — 对于单结果,使用带缓冲通道可以避免接收者提前退出时协程泄漏
ch := make(chan Result, 1)
go func() { ch <- compute() }()
result := <-ch
// ❌ 带有忙等待 default 的 select
for {
select {
case v := <-ch:
process(v)
default:
// 空转
}
}
// ✅ — 除非确实需要非阻塞,否则使用阻塞 select
for v := range ch {
process(v)
}
使用 golang.org/x/sync/errgroup 进行带错误收集的扇出操作。
4. 结构体与接口 {#structs}
// ❌ 巨大的接口
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
Delete(key string) error
List(prefix string) ([]string, error)
// ... 还有 10 个方法
}
// ✅ — 小型的、可组合的接口
type Getter interface { Get(key string) ([]byte, error) }
type Setter interface { Set(key string, val []byte) error }
type Storage interface { Getter; Setter }
// ❌ 从构造函数返回具体结构体(将调用者与实现绑定)
func NewStore() *RedisStore { ... }
// ✅ — 当你已经或预计有多个实现时,返回接口
func NewStore() Storage { return &RedisStore{...} }
// ❌ 对小值类型使用指针接收者
func (p *Point) X() float64 { return p.x }
// ✅ — 对小而不可变类型使用值接收者
func (p Point) X() float64 { return p.x }
规则:当方法会修改状态或者结构体较大(>3 个非平凡大小的字段)时使用指针接收者,否则使用值接收者。
5. 函数与闭包 {#functions}
// ❌ 仅仅为了避免声明变量而使用命名返回值
func divide(a, b float64) (result float64, err error) {
result = a / b
return
}
// ✅ — 命名返回值只有在用于延迟修改或文档说明时才值得使用
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
// ❌ 闭包捕获循环变量(经典 Go 错误,Go 1.22+ 已修复)
// Go <1.22:每个协程捕获同一个 i
for i := 0; i < n; i++ {
go func() { use(i) }()
}
// ✅ (Go <1.22 — 将变量作为参数传递)
for i := 0; i < n; i++ {
go func(i int) { use(i) }(i)
}
// Go 1.22+:循环变量的作用域限定在每次迭代中,因此原始写法是安全的
6. Go 语言特定反模式 {#antipatterns}
| 反模式 | 推荐做法 |
|---|---|
连续 5 次及以上重复 if err != nil { return err } | 可以接受——这是地道的 Go 写法 |
对预期错误使用 panic | return err |
带有副作用的 init() | 在 main 或构造函数中进行显式初始化 |
不使用泛型的 interface{} / any | 使用泛型(Go 1.18+)或类型化接口 |
| 互斥锁字段未与其保护的数据相邻 | 将 mu 直接放在它守卫的字段上方 |
| 通道的通道 | 通常是过度设计的迹象;重新设计 |
在测试中使用 time.Sleep | 使用 testing 钩子或通道进行同步 |
| 导出的类型带有未导出的字段(当字段是核心内容时) | 使用全部字段均为导出类型的 record 风格结构体 |
在 main 函数之外使用 log.Fatal | 将错误向上返回 |
限制
- 这些是语言特定的指南,不涵盖整体架构决策。
- 过度压缩可能会降低可读性;请酌情判断。