Effective Go 精要

翻译整理自 Effective Go 与 Go Code Review Comments。

本章总结官方推荐的 Go 编程习惯与最佳实践。

1. 格式化

统一使用 gofmt / go fmt 格式化代码。不要自定义风格争议,让工具决定。

go fmt ./...

更强大的工具 gofumpt、goimports(补全 import、整理顺序):

go install golang.org/x/tools/cmd/goimports@latest

2. 注释

注释应该是完整句子,以被注释的名字开头:

func Add(a, b int) int { return a + b }

文档以 // 开头,pkg 文档放在 doc.go 文件中。

3. 命名

  • 驼峰命名:httpClient、HttpServer
  • 首字母大小写决定可见性
  • 缩写词统一大小写:URL、API、ID
  • 接收者:1-2 字母短名(如 r、c),保持一致
  • 接口:通常 -er 后缀(Reader、Writer、Closer)

4. 控制结构

  • 没有 while、do-while,只有 for
  • if 可带短语句
  • switch 默认 break,不需要写
  • 用 break Loop 跳出多层

5. 错误处理

不要忽略错误

// ❌
file, _ := os.Open(path)

// ✅
file, err := os.Open(path)
if err != nil {
    return err
}
defer file.Close()

哨兵错误

if errors.Is(err, io.EOF) {
    // ...
}

自定义错误

var ErrNotFound = errors.New("not found")

if errors.Is(err, ErrNotFound) {
    // handle
}

包装错误

return fmt.Errorf("query failed: %w", err)

用 %w 包装,外层可用 errors.Is / errors.As 解开。

6. defer 配对

资源释放必配 defer,且紧跟在错误检查后:

f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

7. 包设计

  • 一个包一个职责
  • 包名简短、小写、单数
  • 公开 API 最小化
  • 接受接口、返回结构体
  • 包应无状态、可重入

8. 并发

  • 优先 channel
  • 启动的 goroutine 必须有退出路径,避免泄漏
  • 使用 context.Context 传递超时与取消
  • 共享状态用 sync.Mutex/RWMutex
  • 不要在 main 启动 goroutine 后直接返回

9. 内存与性能

  • 不要过早优化
  • 避免不必要的分配:bytes.Buffer、strings.Builder、sync.Pool
  • make 切片时尽量预估容量,减少扩容拷贝
  • append 返回必须接收
  • struct 用 * 还是值:根据大小、是否修改
var b strings.Builder
for i := 0; i < 1000; i++ {
    b.WriteString("x")
}
b.String()

10. 测试与可维护性

  • 写表驱动测试
  • benchmark 验证性能改动
  • 用 golangci-lint 做静态检查
  • 公开 API 写 Example

11. go vet 与 linter

go vet ./...
go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run

12. 项目布局建议

参考 golang-standards/project-layout:

.
├── cmd/        # 可执行入口
├── internal/   # 仅本仓库可用的私有代码
├── pkg/        # 可被外部引用的库
├── api/        # API schema
├── configs/    # 配置
├── scripts/    # 脚本
└── go.mod

internal/ 是 Go 强约束:只能被 internal/... 父级以上的模块导入。

13. 常见陷阱

  • for 循环闭包:循环变量在 Go 1.22 前会被复用,闭包需传入参数
  • map 并发:并发读写 panic,用 sync.Map 或锁
  • nil interface:interface(nil) 不是 nil,类型与值同时为 nil 才相等
  • defer 参数立即求值
  • 向已关闭 channel 发送会 panic
  • 空切片 vs nil 切片:序列化、反射行为不同

小结

简洁、清晰、一致,是 Go 的灵魂。

Go 不追求语言特性的炫技,而追求工程化、可读性、可维护性。读完本章,你已经具备写出地道 Go 代码的全部要素。