我在 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 中进行显式构建,并声称无环图和手动清理仍然是可管理的。