设计哲学
Go 社区在长期实践中沉淀了大量简洁有力的格言(Proverbs)。 它们既是设计哲学,也是代码审查时的口头禅。
本文收集了三类内容:
- 官方 Go Proverbs:Rob Pike 在 Gopherfest 2015 上提出的经典 19 条;
- The Zen of Go:Dave Cheney 总结的 Go 之道;
- 设计与实践格言:Effective Go、Code Review Comments 及社区广为流传的原则。
官方 Go Proverbs
以下 19 条来自 go-proverbs.github.io,由 Rob Pike 在 Gopherfest SV 2015 演讲中提出,被视为 Go 语言最核心的指导思想。
Don't communicate by sharing memory, share memory by communicating.
不要通过共享内存来通信,要通过通信来共享内存。
优先使用 channel 协调 goroutine,而不是用锁保护共享状态。
Concurrency is not parallelism.
并发不等于并行。
并发是程序结构,并行是执行方式;好的并发设计不一定需要多核并行。
Channels orchestrate; mutexes serialize.
Channel 用于编排,互斥锁用于串行化。
channel 表达协作关系,mutex 保护临界区;各司其责。
The bigger the interface, the weaker the abstraction.
接口越大,抽象越弱。
小接口(如
io.Reader)更稳定、更易复用、更容易 mock。Make the zero value useful.
让零值变得有用。
sync.Mutex、空切片、空 map 都应能在未初始化时直接使用。interface{} says nothing.空接口什么都没说。
any 放弃了编译期类型检查,只在真正需要任意类型时使用。Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.
没人最喜欢 gofmt 的风格,但人人都喜欢 gofmt。
统一格式消灭风格争论,让代码审查聚焦真正重要的问题。
A little copying is better than a little dependency.
少量复制优于少量依赖。
与其为了几行工具函数引入整个库,不如直接把那几行代码复制进来。
Syscall must always be guarded with build tags.
Syscall 必须始终用构建标签保护。
平台相关代码要通过
//go:build 限定操作系统,避免跨平台编译失败。Cgo must always be guarded with build tags.
Cgo 必须始终用构建标签保护。
Cgo 破坏跨平台与纯 Go 工具链,需要显式隔离。
Cgo is not Go.
Cgo 不是 Go。
一旦使用 Cgo,就进入了 C 的内存模型、构建系统和平台依赖。
With the unsafe package there are no guarantees.
使用
unsafe 包没有任何保证。unsafe 绕过类型系统,版本升级时可能无声失效。Clear is better than clever.
清晰胜于取巧。
可读性优先于炫技;半年后能一眼看懂的代码才是好代码。
Reflection is never clear.
反射从来都不清晰。
反射增加运行时风险、降低性能、破坏类型安全;能不用就不用。
Errors are values.
错误是值。
error 只是普通的返回值,可以包装、聚合、延迟处理,而不是异常。Don't just check errors, handle them gracefully.
不要只检查错误,要优雅地处理它们。
避免
if err != nil { return err } 的机械传播,思考错误该如何处理。Design the architecture, name the components, document the details.
设计架构,命名组件,记录细节。
好名字是设计的一部分;文档面向用户,而非重复代码。
Documentation is for users.
文档是写给用户看的。
注释说明「为什么」和「怎么用」,而不是复述「做了什么」。
Don't panic.
不要 panic。
panic 只用于真正的不可恢复错误;正常流程通过返回 error 处理。The Zen of Go
Dave Cheney 在 《The Zen of Go》 中提出 11 条实践原则,强调如何在工程中以 Go 的方式思考。
Each package fulfils a single purpose.
每个包只承担单一职责。
包是 Go 的编译和复用单元,保持小而专注。
Handle errors explicitly.
显式处理错误。
错误路径绝不隐藏,调用方始终能看到可能失败的地方。
Return early rather than nesting deeply.
尽早返回,避免深层嵌套。
错误先返回,成功路径保持在左侧,降低缩进层级。
Leave concurrency to the caller.
把并发决策交给调用方。
库函数通常应同步;是否启动 goroutine 由调用方决定。
Before you launch a goroutine, know when it will stop.
启动 goroutine 前,要知道它何时停止。
goroutine 泄漏是生产环境常见问题,必须设计退出机制。
Avoid package level state.
避免包级状态。
包级变量导致隐藏依赖和测试困难;优先依赖注入。
Simplicity matters.
简洁很重要。
少即是多;Go 故意不提供多种方式完成同一件事。
Write tests to lock in the behaviour of your package's API.
编写测试以固定包 API 的行为。
测试是契约,防止重构时无意中破坏对外承诺。
If you think it's slow, first prove it with a benchmark.
如果觉得慢,先用基准测试证明。
不要凭感觉优化;
go test -bench 是性能决策的前提。Moderation is a virtue.
适度是一种美德。
任何模式滥用都会变成反模式;保持克制。
Maintainability counts.
可维护性至关重要。
代码被阅读的次数远多于被编写的次数。
设计哲学
这些格言来自 Go 作者访谈、Effective Go 以及社区总结,描述的是 Go 在世界观层面的选择。
Less is more.
少即是多。
Go 刻意做减法:没有泛型直到 1.18、没有类继承、没有异常。更少的特性意味着更少的决策、更低的学习成本和更稳定的代码库。
Explicit is better than implicit.
显式优于隐式。
错误必须显式返回、导入必须显式使用、指针必须显式操作。Go 讨厌「魔法」。
Composition over inheritance.
组合优于继承。
Go 用结构体嵌入和隐式接口实现代码复用,避免脆弱的基类层次结构。
Accept interfaces, return structs.
接受接口,返回结构体。
函数参数用接口保持灵活,返回值用具体类型避免抽象泄漏。
Define interfaces where they are consumed, not where they are implemented.
在使用方定义接口,而不是在实现方定义。
这样接口自然保持最小,实现者无需感知接口存在,降低耦合。
Program to an interface, not to an implementation.
面向接口编程,而非面向实现编程。
依赖抽象让测试替换、功能扩展和长期维护都更容易。
工程实践
来自 Go Code Review Comments、Effective Go 和 go-advice 的实用建议。
| 英文 | 中文 | 说明 |
|---|---|---|
Always go fmt your code. | 始终用 go fmt 格式化代码。 | 格式一致是团队协作的底线。 |
| Handle errors first. | 先处理错误。 | 减少嵌套,让成功路径左对齐。 |
Pass context.Context first. | 把 context.Context 作为第一个参数。 | 约定俗成,便于取消、超时和链路追踪。 |
| Use table-driven tests. | 使用表驱动测试。 | 一组输入输出集中管理,减少重复代码。 |
Prefer chan struct{} over chan bool for signals. | 信号 channel 优先用 chan struct{}。 | 明确表示只关心事件,不关心值。 |
Move defer to the top. | 把 defer 放在函数顶部。 | 资源获取与释放靠近,降低遗漏风险。 |
Wrap errors with fmt.Errorf("...: %w", err). | 用 %w 包装错误。 | 保留原始错误,便于上层判断和链路追踪。 |
Use time.Duration instead of int64 + variable name. | 用 time.Duration 表示时长。 | 类型安全,避免毫秒/秒混淆。 |
Don't use checkErr which panics or os.Exit. | 不要用会 panic 或退出的 checkErr。 | 库代码绝不能擅自终止进程。 |
| Check interface implementation at compile time. | 编译期检查接口实现。 | var _ io.Reader = (*MyReader)(nil) |
| Profile before optimizing. | 优化前先分析。 | go tool pprof 和 go test -bench 是标准动作。 |
| Few params of the same type can be defined in a short way. | 同类型连续参数可简写。 | func f(a, b int, s, p string) |
| Reading nonexistent key from map will not panic. | 读取不存在的 map 键不会 panic。 | 推荐 v, ok := m[k] 明确判断。 |
Be careful with range in Go. | 注意 range 的拷贝行为。 | for i, v := range a 会拷贝元素,range &a 不会。 |
:::tip 一句话总结
Go 的谚语可以概括为:少做选择、显式表达、错误是值、组合优先、清晰胜于聪明。
把这几条记在心上,写出的 Go 代码自然不会太差。
:::