- 后端
- 开发工具
【免费下载链接】gotenberg
A developer-friendly API for converting many document formats into PDF files, and more!
Gotenberg 是一个基于 Docker 的文档转 PDF API,本文以其仓库根目录下的贡献者指南(CLAUDE.md)为骨架,结合 Makefile、模块系统源码、入口与标准装配、遥测与日志实现 等仓库真实代码,系统讲解该项目的模块架构、构建验证命令、代码与文档规范、测试体系以及 PR 提交流程。读完本文,你将掌握如何为 Gotenberg 新增一个功能模块、如何在本地完成全部质量检查,以及如何写出符合项目标准、可通过 Review 的合并请求。
项目定位与两条铁律
Gotenberg 是“开发友好”的文档转换 API:通过 HTTP 接口接收文件,输出 PDF,内部依托 Chromium、LibreOffice 以及 qpdf、pdfcpu、pdftk 等引擎完成渲染与加工。对任何贡献者而言,有两条规则凌驾于其他一切规则之上:
- 向后兼容(Backward compatibility):未经讨论,绝不重命名或删除 CLI 标志、环境变量、API 表单字段或 HTTP 端点。
- 防御式编程(Defensive programming):假定输入是畸形/恶意的,显式处理所有错误,生产路径绝不 panic。
这两条铁律贯穿了后续全部代码规范,是阅读本指南其余内容的前提。任何改动若违反向后兼容,必须在 PR 描述中标记为 breaking change(见下文 Pull Requests)。
开发工具链
贡献 Gotenberg 需要以下工具,它们共同构成"改动-构建-检查-测试"的完整闭环:
| 工具 | 说明 |
|---|---|
| Go 模块 | 模块名为github.com/gotenberg/gotenberg/v8,详见 go.mod(当前 Go 版本 1.27.1,含 chromedp、godog、echo、OpenTelemetry、testcontainers 等核心依赖) |
| Go | 版本以 go.mod 中go指令为准 |
| Docker | 构建镜像与运行集成测试的容器编排基础 |
| Node.js | 用于 Prettier 对非 Go 文件(Markdown、YAML、JSON)的格式化与 lint |
| golangci-lint | v2 及以上版本,Go 代码静态检查,要求零告警 |
注意指南明确要求:所有构建与验证任务一律经由 Makefile,不要直接执行go命令,除非正在调试某个特定包。原因是 Makefile 集中封装了统一的构建参数(镜像仓库、版本号、Dockerfile 目标)与测试参数(集成测试 tag、超时、重试逻辑)。
动工之前:变更流程约定
对于非平凡改动,Gotenberg 要求先开 issue 或 draft PR,并在其中说明:
- 需要改什么、为什么;
- 拟定方案:涉及哪些文件、接口变更、新增表单字段;
- 受影响的集成测试 tag 有哪些(tag 清单见 Makefile 中
test-integration目标上方的注释,包括health、chromium-convert-html、merge、split、webhook、download-from等数十个)。
其余约定包括:
- 一个 PR 只做一件事:feature、bug fix、refactoring 分开提交,避免互相纠缠。
- 先写 Gherkin 场景,再写 Go 代码:新增功能或路由时,集成测试的行为规格要先行。
- 路由变更必须同步更新 Bruno 集合(
.bruno/,该目录下的 README.md 描述.bru文件格式与路由更新检查清单),保证 API 集合与每一个路由保持一致。
项目目录布局
仓库按职责划分为清晰的六块(完整文件清单见仓库根目录):
cmd/gotenberg/ -> 入口点(装配/启动),不含业务逻辑 pkg/gotenberg/ -> 核心模块系统、接口、工具、mocks pkg/modules/ -> 功能模块(api、chromium、libreoffice、pdfengines 等) pkg/standard/ -> 通过 imports 装配全部标准模块 test/integration/ -> Gherkin feature 文件 + Go 测试基础设施 build/ -> Dockerfile、字体、Chromium 配置 .bruno/ -> Bruno API 集合(镜像每一个路由)核心接口集中在 pkg/gotenberg/modules.go:Module、Provisioner、Validator、Debuggable(此外还有App、SystemLogger)。每个模块实现Descriptor(),并通过init()自我注册。各目录职责如下:
cmd/gotenberg/只是装配入口:main.go 唯一做的是导入pkg/standard触发模块注册,然后调用gotenbergcmd.Run()。业务逻辑严禁出现在这里。pkg/modules/是功能实现的主战场,例如chromium/(浏览器渲染、网络聚合、SSRF 防护代理)、libreoffice/(含api/子包与pdfengine/子包)、pdfengines/(多引擎合并/拆分/加密等编排)、webhook/、prometheus/、qpdf/、pdfcpu/、pdftk/、exiftool/等。test/integration/features/存放所有.feature行为规格文件,test/integration/scenario/存放对应 step 定义与容器编排逻辑。
构建与验证:Makefile 命令速查
所有开发任务都汇总在 Makefile 中,下表是贡献指南给出的核心命令与使用时机:
| 命令 | 用途 | 使用时机 |
|---|---|---|
make build | 构建 Gotenberg Docker 镜像 | 集成测试或手动测试之前 |
make run | 通过docker compose运行 Gotenberg 容器 | 手动测试;标志通过 Makefile 变量与 compose.yaml 配置 |
make telemetry | 启动 OpenTelemetry collector 与 OpenObserve | 本地测试遥测时 |
make down | 停止全部 compose 容器 | 手动测试之后 |
make godoc | 在localhost:6060提供 GoDoc | 验证文档 |
make fmt | 格式化 Go 代码 | 提交之前 |
make lint | 检查 Go 代码(不允许任何告警) | 提交之前 |
make prettify | 格式化非 Go 文件(Markdown、YAML、JSON) | 提交之前 |
make lint-prettier | 检查非 Go 文件 | 提交之前 |
make test-unit | 运行单元测试 | 提交之前 |
make test-integration | 运行全部集成测试(40 分钟超时) | 提交之前 |
对照 Makefile 源码,还能补充几个有价值的事实:
make build支持通过TARGET变量选择镜像变体(gotenberg-chromium或gotenberg-libreoffice),并借助.env中注入的DOCKER_REGISTRY、DOCKER_REPOSITORY、GOTENBERG_VERSION、DOCKERFILE等变量完成构建。make test-unit实际执行go test -race ./...,即带竞态检测跑全部包。make test-integration实际执行go test -timeout 40m -tags=integration ...,通过--gotenberg-docker-repository、--gotenberg-version、--no-concurrency、--tags等参数把镜像与 tag 传给 test/integration 的测试入口,并且失败场景最多自动重试 3 次。make fmt是一个组合动作:go fix -errorsastype=false ./...(禁用会把errors.As改写为不兼容代码的 modernizer)、golangci-lint fmt与go mod tidy(依赖“优化”)。- Makefile 底部还罗列了数十个
GOTENBERG_*、API_*、CHROMIUM_*、LIBREOFFICE_*、PDFENGINES_*、WEBHOOK_*等环境变量默认值,本地手动测试时可以直接覆盖,例如API_PORT、API_TIMEOUT、CHROMIUM_MAX_CONCURRENCY=6、PDFENGINES_MERGE_ENGINES=qpdf,pdfcpu,pdftk等。
只跑相关的集成测试 tag
完整集成测试套件超时上限为 40 分钟,因此只运行与本次改动相关的 tag,而不是全量执行:
make test-integration TAGS=health make test-integration TAGS=chromium-convert-html make test-integration TAGS="merge,split"tag 之间用逗号分隔、整体用引号包裹。tag 全集(chromium、libreoffice、pdfengines-*、factur-x、prometheus-metrics、root、version、webhook、download-from等)以注释形式维护在 Makefile 中。
代码规范
模块系统:受 CaddyServer 启发的自注册架构
Gotenberg 采用自注册模块架构,灵感来自 CaddyServer。每个模块位于pkg/modules/<name>/,至少要实现gotenberg.Module(即Descriptor()方法),并通过init()自我注册;模块间的装配发生在pkg/standard/。
modules.go 给出了Module与ModuleDescriptor的定义与用法示例:
// Module is a sort of plugin which adds new functionalities to the application // or other modules. // // type YourModule struct { // property string // } // // func (YourModule) Descriptor() gotenberg.ModuleDescriptor { // return gotenberg.ModuleDescriptor{ // ID: "your_module", // FlagSet: func() *flag.FlagSet { // fs := flag.NewFlagSet("your_module", flag.ExitOnError) // fs.String("your_module-property", "default value", "flag description") // return fs // }(), // New: func() gotenberg.Module { return new(YourModule) }, // } // } type Module interface { Descriptor() ModuleDescriptor }ModuleDescriptor的三个字段中,ID是唯一标识(snake_case,必填),FlagSet定义模块自己的 CLI 标志(可选),New返回模块类型的全新实例(必填)。注册通过init()完成:
func init() { gotenberg.MustRegisterModule(YourModule{}) }MustRegisterModule会做防御式校验:ID 为空、New为 nil、New()返回 nil、ID 重复都会直接 panic 拒绝注册;GetModuleDescriptors则按 ID 排序返回全部已注册模块的描述符。实际的标准装配可见 pkg/standard/imports.go:它通过空白导入依次注册api、chromium、exiftool、libreoffice、libreoffice/api、libreoffice/pdfengine、pdfcpu、pdfengines、pdftk、prometheus、qpdf、webhook共 12 个模块。
判断一个功能归属哪个模块的原则:先评估是否属于既有模块,只有真正独立的关注点才新建模块。新增模块后,需要在其主文件中写init()自注册,并让主程序空白导入该模块路径(见 modules.go 中MustRegisterModule的文档示例)。
向后兼容
CLI 标志、环境变量、API 表单字段、HTTP 端点,以及会改变既有行为的默认值,未经讨论一律不得更改。需要淘汰旧名称时,用fs.MarkDeprecated()标记废弃,并让新旧两个名称并行注册,过渡期后由维护者决定移除时机。任何违反向后兼容的改动,都要在 PR 描述中显式标注为 breaking change。
错误处理
- 每个错误都要用
fmt.Errorf("description: %w", err)包裹上下文。 - 绝不静默吞掉错误。
- 错误匹配必须用
errors.Is,严禁用strings.Contains做字符串比较。 - 生产代码路径禁止 panic。
- 输入校验要防御式进行。
这些规则同样体现在MustRegisterModule等内部实现对畸形注册的拒绝方式上:宁可早期 panic(开发期编程错误)也不要在运行时带病运行。
错误消息的三类受众
面向客户端(HTTP 响应体)与面向运维人员(启动日志、Provision、Validate)的错误消息,要说明"什么失败了、失败原因(不明显时)、如何修复(存在修复方案时)":
- 客户端:点名违规的表单字段及其合法取值。永远不要返回裸的
http.StatusText()。 - 运维人员:指出需要设置的环境变量或标志,以及被检查的路径或取值。
- 安全与过滤类错误:对客户端保持泛化,不泄露 allow/deny 列表或私有 IP 策略;具体原因只记入运维日志。
- 不要出现含糊措辞(如"虽然其他请求可能已失败"),不要在面向人类的修复建议中暴露原始
os.Stat或 exec 输出。
被fmt.Errorf包裹、只出现在日志中的内部错误链不受此限,保持精确与技术性即可。
日志:上下文感知的 slog
在Provision()阶段通过gotenberg.Logger(mod)获取模块专属的 slog logger,其实现见 pkg/gotenberg/telemetry.go:返回全局 logger 并附加logger=<模块 ID>字段,方便按模块过滤。所有日志调用必须是上下文感知的:
logger.DebugContext(ctx, msg) logger.InfoContext(ctx, msg) logger.ErrorContext(ctx, msg)使用...Context变体的关键收益:当 OpenTelemetry 启用时,trace/span ID 会随上下文自动传播进结构化日志,形成"日志-追踪"的关联能力。此外 pkg/gotenberg/telemetry.go 中的StartTelemetry会把标准 slog handler 与 OTel 的 tracer/meter/logger provider 一并初始化,日志级别、格式(auto/json/text)、级别大小写(lower/upper)等都由TelemetryConfig校验后生效。
遥测:外部工具调用必须打 span
对 Chromium、LibreOffice、PDF 引擎、webhook、下载等外部工具调用,必须创建 OpenTelemetry span:
- span kind 用
trace.SpanKindClient; - 用
semconv.ServerAddress("toolname")标记对端服务名; - 通过
gotenberg.Tracer()获取 tracer、gotenberg.Meter()获取 meter(两者均已预置 instrumentation name 与版本,见 pkg/gotenberg/telemetry.go)。
对应的语义约定实现位于 pkg/gotenberg/semconv,server.go与client.go分别承载服务端与客户端侧的 span 属性构造。make telemetry可一键启动 OpenTelemetry collector 与 OpenObserve 容器用于本地观察(配置见 otel-collector-config.yaml 与 compose.yaml)。
导入顺序
由gci强制:标准库一组、第三方一组、github.com/gotenberg/gotenberg/v8一组,三组之间用空行分隔。例如 pkg/standard/imports.go 与 cmd/gotenberg/main.go 的导入区都遵循这一分组。
文档规范
语气
- 短句、陈述句:先说它做什么,然后停笔。
- 动作开头:"Validates font embedding" 而非 "This function validates font embedding"。
- 主动语态:"Gotenberg checks the profile" 而非 "The profile is checked by Gotenberg"。
- 不用破折号(em dash),改用句号、冒号或逗号。
- 不用 "we" 式含糊:直接写 "Don't...",而不是 "We do not recommend..."。
Godoc 注释
每个导出的类型和函数都必须有以标识符名称开头的 Godoc 注释:
// OutboundDecision is the result of validating an outbound URL via // [DecideOutbound]. ... type OutboundDecision struct { ... } // DialPinned dials each addr in turn until one connects, returning the // first successful connection or the last error. ... func DialPinned(ctx context.Context, network string, addrs []netip.Addr, port string) (net.Conn, error)每个包都要有doc.go,内含// Package foo ...注释:
// Package api manages a LibreOffice instance via the UNO API. package api标识符用[Name]方括号引用,便于 pkg.go.dev 自动链接:
// Callers pass the Pinned slice from [OutboundDecision] so that the dial // targets exactly the IPs that [DecideOutbound] resolved, preventing DNS // rebinding between validation and connect.代码注释
- 解释why,而不是what。
- 不用编号步骤注释(
// 1. Do X)。 - 不用带编号的分隔注释(
// --- 8. Foo ---);纯分隔线可用于主要边界。 - 不写复述代码的噪音注释(如
// Check if err is nil)。 - 相关时引用规范条款(如
// Per ISO 32000-2, Table 116...)。 - 技术债统一标注为
// TODO: [context],配合make lint-todo可扫描全部 TODO。
测试体系
单元测试
单元测试采用表驱动(table-driven)风格,写在对应的*_test.go文件中。优先复用 pkg/gotenberg/mocks.go 中现成的综合 mock 实现,而不是为每个用例新造一套。仓库中各包均配有成对测试,例如 modules_test.go、flags_test.go、context_test.go 等。
集成测试
集成测试使用 BDD 框架 Gherkin(由 Godog 驱动,go.mod 中依赖github.com/cucumber/godog v0.16.0),并用testcontainers-go做 Docker 编排(go.mod 中依赖testcontainers/testcontainers-go v0.44.0):
- feature 文件位于 test/integration/features/,例如
chromium_convert_html.feature、pdfengines_merge.feature、webhook.feature、prometheus_metrics.feature等,每个文件对应一个或多个测试 tag。 - step 定义位于 test/integration/scenario/,写新测试前应先阅读 scenario.go 与 containers.go 了解既有模式。
- 运行集成测试前必须先
make build构建镜像;完整套件有 40 分钟超时,因此只跑与改动相关的 tag。
Pull Requests 提交流程
提交信息
遵循 [Conventional Commits] 规范:<type>(<scope>): <description>。常用类型:feat、fix、refactor、test、docs、chore、ci、build;scope 与改动所属模块或领域一致,例如chromium、pdfengines、api。
只 stage 具体文件,严禁git add -A或git add .,避免把无关改动混入提交。
提交前检查清单
打开 PR 前逐项确认:
- 无向后兼容性回归,见上文"向后兼容"。
- 满足代码规范:错误包裹、日志上下文、遥测 span、导入顺序、无 panic、
cmd/中无业务逻辑。 - 满足文档规范:每个导出标识符都有 Godoc、新包有
doc.go、语气符合要求。 make fmt && make lint && make prettify && make lint-prettier零告警通过。make test-unit通过。- 相关的
make test-integration TAGS=...通过。 - 新增或修改路由时,Bruno 集合已同步更新。
延伸阅读
仓库内还有三份与贡献流程直接相关的文档,建议深入阅读:
- test/integration/README.md:Gherkin step 参考、全部可用 tag、如何编写新集成测试。
- .bruno/README.md:
.bru文件格式、Bruno 集合编写约定与路由更新检查清单。 - pkg/modules/pdfengines/README.md:如何为 PDF 引擎新增功能(对应的 Makefile 变量与标志)。
- 后端
- 开发工具
【免费下载链接】gotenberg
A developer-friendly API for converting many document formats into PDF files, and more!
相关推荐
Gotenberg 开发者贡献指南:模块架构、代码规范、测试体系与 Makefile 工作流全解
Gotenberg 开发者贡献指南:模块架构、代码规范、测试体系与 Makefile 工作流全解 Gotenberg 是一个基于 Docker 的文档转 PDF
后端开发工具Gotenberg 仓库贡献指南:从模块架构、代码规范到集成测试的完整开发守则
Gotenberg 仓库贡献指南:从模块架构、代码规范到集成测试的完整开发守则 本文以 Gotenberg 仓库的 AGENTS.md https://link
后端开发工具Gotenberg 贡献指南:模块架构、代码规范与集成测试实战
Gotenberg 贡献指南:模块架构、代码规范与集成测试实战 Gotenberg 是一个基于 Docker 的文档转 PDF API 服务,本指南以其仓库内的
后端开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考