我在 13 年后如何用 Go 编写 HTTP 服务
Go 开发者正在讨论如何组织 HTTP 服务,使其既可测试又易维护,重点包括尽量减少 `main` 中的逻辑、将依赖显式传入处理器,以及避免庞大的“上帝”式配置或 server 结构体。许多人偏好小而可组合的函数和显式依赖注入(有时借助 Uber 的 fx 等库),而另一些人则警惕过度工程化,更喜欢用于特定服务的、更像脚本的 `main`。一个反复出现的主题是使用类型和解析(例如“parse, don’t validate”)来编码不变量并减少 bug,而不是依赖散落在各处的临时验证。
main 的结构与可测试性
- 许多人喜欢一个很小的
main(),它把工作委托给run()函数,这样错误就可以被返回,大部分逻辑也变得可测试、可复用。 - 也有人更喜欢用于内部服务的“扁平脚本”式
main.go,让顶层控制流保持可见,而不是被塞进辅助函数里。 - 形成的共识是:把不可测试/引导启动代码保持得尽可能小,但不要以可读性为代价;
main()本身不能被测试调用,这促使人们引入一层间接调用。
处理器、依赖与 DI
- 大力支持显式依赖:处理器应该通过参数或按处理器划分的结构体获取所需内容,而不是把它们藏在一个巨大的 server 结构体里。
- 有些人追求“极致清晰”,即使这意味着很多参数;另一些人则把依赖打包到 context/env 结构体或处理器结构体中,以避免签名过于庞大。
- 这里存在简单性与耦合之间的张力:庞大的“上帝”式 server 结构体,以及带着很多参数的构造函数,都有过度耦合的味道。
配置与 options 模式
- 传递一个可变的全局配置对象四处流转,几乎普遍受到批评;它会把子系统耦合在一起,还可能引发微妙的顺序 bug。
- 更受欢迎的模式包括:不可变或“冻结”的配置;把配置复制到内部字段;按包划分的配置结构体;以及构造函数使用“functional options”或 option 结构体。
- 顾虑在于:过于巧妙的 options 模式会增加样板代码并降低可发现性;从长期看,简单的配置结构体往往更占优势。
验证 vs “parse, don’t validate” 与类型
- 许多人主张把基础类型(例如
Username)包装成专门类型,并通过解析/验证构造它们,以让非法状态不可表示。 - 对 Go 的零值存在争论:如果结构体可以在不经过构造函数的情况下被实例化,它们就会削弱这些保证。
- 有些人认为这种模式在 Go 里过度设计或过于笨重;另一些人则认为它对于避免“stringly typed” bug 至关重要。
测试策略
- 一些人倾向于端到端风格的测试:启动一个带有伪造依赖的测试服务器,覆盖 HTTP 和中间件路径。
- 另一些人则专注于通过注入依赖和使用小的辅助函数,对处理器逻辑进行单元测试。
先规格后代码与代码生成
- 有些参与者喜欢 OpenAPI-first 工作流配合生成器(例如 ogen、oapi-codegen),以避免重复的验证和路由代码。
- 另一些人觉得编写 OpenAPI 很繁琐,但指出工具与 LLM 辅助编写有所帮助,而且 IDL(包括 gRPC/gateway)可以很高效。
依赖注入框架
- 轻量级 DI 框架(例如 fx)因能够处理复杂的初始化/关闭图而受到赞赏。
- 怀疑者则认为 DI 框架增加了间接层;他们更喜欢在
main中进行显式构建,并声称无环图和手动清理仍然是可管理的。