- 后端
- 开发工具
【免费下载链接】gotenberg
A developer-friendly API for converting many document formats into PDF files, and more!
Gotenberg 是一个基于 Docker 的文档转 PDF API 服务,本指南以其仓库内的 CONTRIBUTING.md 为骨架,结合源码与测试基础设施,完整讲解贡献者需要掌握的两条铁律、模块系统架构、Makefile 工作流、编码与文档规范,以及 Gherkin 驱动的集成测试实战。读完本文,你将具备向 Gotenberg 提交高质量 Pull Request 的完整方法论:从提出 issue、编写 feature 文件、实现模块,到通过make lint与make test-integration TAGS=...的完整闭环。
两条覆盖一切的最高原则
无论改动多小,以下两条规则凌驾于其他所有约定之上:
- 向后兼容(Backward compatibility):未经讨论,绝不重命名或删除 CLI 标志、环境变量、API 表单字段或 HTTP 端点。Gotenberg 是面向开发者的 API 服务,任何破坏现有调用方式的改动都会波及所有下游用户。
- 防御性编程(Defensive programming):假设输入永远是畸形的,显式处理每个错误,生产代码路径中绝不 panic。
这两条原则在仓库中贯穿始终:错误处理、日志、遥测与文档规范都围绕它们展开。
工具链要求
参与开发前,请确认环境满足以下要求:
| 工具 | 要求 |
|---|---|
| Go 模块 | github.com/gotenberg/gotenberg/v8 |
| Go 版本 | 以 go.mod 为准(当前仓库为go 1.27.1) |
| Docker | 构建镜像与运行集成测试必需 |
| Node.js | 版本见.node-version,用于 Prettier 非 Go 代码格式化 |
| golangci-lint | v2 及以上 |
go.mod中还值得注意:Chromium 依赖chromedp被显式固定在 v0.14.2,并附注释说明 v0.15.x 会破坏 headless 打印模式的绘制管线(rAF / ResizeObserver / IntersectionObserver 停止触发,导致图表空白),这是向后兼容原则在依赖层面的直接体现。
动手之前:先讨论,再编码
对于非平凡的改动,Gotenberg 要求先打开一个 issue 或草稿 PR(draft PR),在其中说明:
- 需要改变什么;
- 提议的解决方案:涉及哪些文件、哪些接口变更、哪些表单字段;
- 会受影响的集成测试标签(integration test tags)。
同时遵循两条协作纪律:
- 一个 PR 只做一件事。特性、缺陷修复、重构必须拆分为独立的 PR,便于审查与回滚。
- 先写场景,后写代码。新增功能或路由时,先编写 Gherkin 场景(
.feature文件),再写 Go 实现;如果路由有变化,还要同步更新 Bruno API 集合(.bruno/)。这种"测试先行"的节奏保证了每个端点都有可验证的行为定义。
项目布局:五层结构一目了然
cmd/gotenberg/ -> 入口点(装配/启动)。不含业务逻辑。 pkg/gotenberg/ -> 核心模块系统、接口、工具、mocks。 pkg/modules/ -> 功能模块(api、chromium、libreoffice、pdfengines 等)。 pkg/standard/ -> 通过 import 装配所有标准模块。 test/integration/ -> Gherkin feature 文件 + Go 测试基础设施。 build/ -> Dockerfile、字体、Chromium 配置。 .bruno/ -> Bruno API 集合(镜像每一个路由)。关键接口都位于pkg/gotenberg/:Module、Provisioner、Validator、Debuggable。每个模块实现Descriptor()并通过init()自注册。
模块系统:Caddy 风格的自注册架构
Gotenberg 的模块架构借鉴了 CaddyServer 的自注册模式,这是理解整个代码库的钥匙。
Module 接口与 Descriptor
每个模块最少实现gotenberg.Module接口,即一个Descriptor()方法。Descriptor()返回ModuleDescriptor,包含三个字段(见 pkg/gotenberg/modules.go):
ID:模块唯一标识(snake_case),必填;FlagSet:模块的 pflag 定义,可选;New:返回模块新实例的构造函数,必填。
典型的模块声明方式如下(摘自Module接口的文档注释):
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) }, } }通过 init() 自注册
模块在其主 Go 文件中通过init()调用gotenberg.MustRegisterModule(...)完成注册。注册时会做三项校验:ID非空、New非 nil、New()返回非 nil 实例;重复 ID 会直接 panic(见 pkg/gotenberg/modules.go)。所有已注册模块的描述符存放在包级 map 中,通过GetModuleDescriptors()按 ID 排序返回。
生命周期:Provision 与 Validate
注册之外,模块可选择性实现两个生命周期接口:
Provisioner:Provision(*Context) error,根据标志、环境变量、上下文初始化模块;Validator:Validate() error,在 Provision 之后校验配置合法性。
Context(见 pkg/gotenberg/context.go)提供了ParsedFlags()获取解析后的标志,以及Module(kind any)/Modules(kind any)按接口类型获取依赖的其他模块实例。当通过Context.Modules()请求某个接口时,尚未初始化的模块会被惰性加载:先调用其Provision,再调用Validate,最后缓存实例(见loadModule,pkg/gotenberg/context.go)。
装配入口:pkg/standard
业务模块本身不直接依赖彼此,装配发生在 pkg/standard/imports.go 中:通过空导入_ "github.com/gotenberg/gotenberg/v8/pkg/modules/..."触发各模块的init()完成注册,覆盖 api、chromium、exiftool、libreoffice(含 api 与 pdfengine 子模块)、pdfcpu、pdfengines、pdftk、prometheus、qpdf、webhook 等标准模块。
最终入口是 cmd/gotenberg/main.go,它只做两件事:导入pkg/standard触发模块注册,然后调用gotenbergcmd.Run()。这就是"入口点不含业务逻辑"原则的落地。
何时新建模块
先判断功能是否属于既有模块,只有在确实是独立关注点时(genuinely separate concern)才创建新模块。cmd/gotenberg/严格用于装配与启动。
Setup 与 Makefile:所有构建验证任务的总入口
Gotenberg 规定所有构建和验证任务都通过 Makefile 完成,除非是调试特定包,否则不要直接运行go命令。
常用命令速查表
| 命令 | 用途 | 使用时机 |
|---|---|---|
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或TARGET=gotenberg-libreoffice变体;make test-unit实际执行go test -race ./...;make lint调用golangci-lint run;make prettify执行npx prettier --write .;make fmt组合了go fix、golangci-lint fmt与go mod tidy。此外 Makefile 顶部还维护着全套环境变量默认值(如API_PORT=3000、CHROMIUM_MAX_CONCURRENCY=6、LIBREOFFICE_RESTART_AFTER=10等),它们通过export导出给 Compose 使用。
只跑与改动相关的集成测试标签
完整集成测试套件有 40 分钟超时,因此应只运行与本次改动相关的标签:
make test-integration TAGS=health make test-integration TAGS=chromium-convert-html make test-integration TAGS="merge,split"集成测试命令本身会自动重试失败的场景最多 3 次。可用标签的完整清单维护在 Makefile 的TAGS变量注释块中,并按 Chromium、LibreOffice、PDF Engines、Infra 等分组,详见下文测试章节。
代码规范(Code conventions)
向后兼容的处理方式
CLI 标志、环境变量、API 表单字段、HTTP 端点,以及任何会改变既有行为的默认值,未经讨论不得更改。需要废弃旧名称时,用fs.MarkDeprecated()标记,并让新旧两个名称并排注册。如果改动确实违反向后兼容,必须在 PR 描述中明确标注为破坏性变更(breaking change)。
错误处理
- 每个错误都用上下文包装:
fmt.Errorf("description: %w", err); - 绝不静默吞掉错误;
- 用
errors.Is匹配错误,绝不用strings.Contains; - 生产代码路径禁止 panic;
- 防御性地校验输入。
从源码看,这套约定在 pkg/gotenberg/cmd.go 中体现得淋漓尽致:Start、Wait、Exec、Kill每个方法都返回带%w包装的错误(如"start unix process: %w"、"context done: %w"),子进程通过Setpgid+SIGKILL整组杀死以避免孤儿进程,且执行外部二进制(soffice、pdftk、qpdf、exiftool、pdfcpu)失败时会映射为有限的 semconverror.type值(context_deadline_exceeded、context_canceled、process_error)。
错误消息规范
面向客户端和运维人员的错误消息要说明"什么失败了、何时不明显时说明原因、存在修复方案时说明如何修复";而只出现在日志中的内部包装错误链不受此限,保持精准和技术化即可。具体分三类:
- 客户端(HTTP 响应体):点名出错的表单字段及其合法取值。绝不返回裸的
http.StatusText(); - 运维人员(启动、
Provision、Validate):点名需要设置的环境变量或标志,以及被检查的路径或值; - 安全与过滤类错误:对客户端保持通用措辞,不泄露 allow/deny 列表或私有 IP 策略;具体原因记入运维日志。
禁止使用推诿性措辞("while others may have failed"),也不要在面向人的修复建议中塞入原始os.Stat或 exec 输出。
日志规范
在Provision()阶段使用gotenberg.Logger(mod)获取模块的 slog logger。所有日志调用必须上下文感知,使用*Context变体:
logger.DebugContext(ctx, msg) logger.InfoContext(ctx, msg) logger.ErrorContext(ctx, msg)这样在 OpenTelemetry 生效时,trace/span ID 才能传播进结构化日志。事实上,Cmd的子进程输出管道(pipeOutput)只有在 debug 级别才启用,且统一走DebugContext(见 pkg/gotenberg/cmd.go)。
遥测规范
所有外部工具调用(Chromium、LibreOffice、PDF 引擎、webhook、下载)都必须创建trace.SpanKindClient类型的 OTEL span,并用semconv.ServerAddress("toolname")标注工具名;trace 与 metrics 分别通过gotenberg.Tracer()和gotenberg.Meter()获取。这与Cmd.Exec()中的实现一致:当 context 携带活跃 span 时,Exec会记录一个"process.exec"客户端 span,携带process.executable.name属性、进程退出码,失败时记录错误与error.type;无父 span 时跳过,避免在请求路径之外产生孤儿根 span(pkg/gotenberg/cmd.go)。
Import 排序
由gci强制:标准库 → 第三方库 →github.com/gotenberg/gotenberg/v8内部包,三组之间以空行分隔。
文档规范(Documentation conventions)
语气(Tone)
- 短句、陈述句:先说它做什么,然后打住;
- 以动作开头:"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.这一约定与仓库中大量包级doc.go文件(如pkg/modules/chromium/doc.go、pkg/modules/pdfengines/doc.go)一一对应。
代码注释
- 解释"为什么",而非"是什么";
- 不用编号步骤注释(
// 1. Do X); - 不用带编号的分节线(
// --- 8. Foo ---),纯分隔线用于明显边界可以; - 不写复述代码的噪音注释(如
// Check if err is nil); - 涉及规范条款时引用出处(如
// Per ISO 32000-2, Table 116...); - 技术债标记为
// TODO: [context]。
测试:单元测试 + Gherkin 集成测试双轨
单元测试
单元测试写在同包*_test.go文件中,采用表驱动(table-driven)风格。应优先复用 pkg/gotenberg/mocks.go 中现成的综合 mock 实现,而不是自己新造轮子。仓库中大量*_test.go(如allowlist_test.go、flags_test.go、pattern_test.go)都是表驱动范式的范例。
集成测试:Godog + testcontainers
集成测试采用 Gherkin(BDD)语法,由 Godog 驱动,配合testcontainers-go做 Docker 编排。关键路径:
- Feature 文件:
test/integration/features/*.feature,每个端点或能力一个文件; - Step 定义:
test/integration/scenario/(容器管理、HTTP 辅助、PDF 校验); - 入口:
test/integration/main_test.go(build tag:integration); - 测试数据:
test/integration/testdata/; - 基础设施:每个场景通过 testcontainers 拉起全新的 Gotenberg Docker 容器;另有一个
gotenberg/integration-tools容器提供 PDF 校验工具(verapdf、pdfinfo、pdftotext)。
运行make test-integration前必须先执行make build,因为集成测试依赖本地构建的 Docker 镜像。完整套件有 40 分钟超时,因此务必只跑与改动相关的标签。
以 test/integration/features/health.feature 为例,一个典型的 Gherkin 场景长这样:
@health Feature: /health Scenario: GET /health Given I have a Gotenberg container with the following environment variable(s): | API_DISABLE_HEALTH_CHECK_ROUTE_TELEMETRY | false | When I make a "GET" request to Gotenberg at the "/health" endpoint Then the response status code should be 200 Then the response body should match JSON: """ { "status": "up", "details": { "chromium": { "status": "up", "timestamp": "ignore" }, "libreoffice": { "status": "up", "timestamp": "ignore" } } } """可用标签分组
集成测试标签按功能域分组(完整清单见 test/integration/README.md):
| 分组 | 标签 |
|---|---|
| Chromium | chromium、chromium-concurrent、chromium-convert-html、chromium-convert-markdown、chromium-convert-url、chromium-screenshot-html、chromium-screenshot-markdown、chromium-screenshot-url、chromium-ssrf |
| LibreOffice | libreoffice、libreoffice-convert、libreoffice-ssrf |
| PDF Engines | pdfengines、pdfengines-convert、pdfengines-merge、merge、pdfengines-split、split、pdfengines-flatten、flatten、pdfengines-optimize、optimize、pdfengines-rotate、rotate、pdfengines-embed、embed、pdfengines-encrypt、encrypt、pdfengines-watermark、watermark、pdfengines-stamp、stamp、pdfengines-metadata、metadata、pdfengines-bookmarks、bookmarks |
| Infra | health、debug、root、version、output-filename、prometheus-metrics、webhook、download-from |
编写新集成测试的步骤
- 在
test/integration/features/创建或更新.feature文件; - 打上合适的标签(如
@chromium @chromium-convert-html); - 新增标签要同时加入 Makefile 的
TAGS注释块和上表; - 新增 step 定义时,把函数加到
scenario/scenario.go,在InitializeScenario注册,并把 step 模式补充到 step 参考文档; - 测试数据放入
test/integration/testdata/。
写新测试前,务必先读scenario.go和containers.go了解既有 step 与容器编排方式。测试中还支持make test-integration NO_CONCURRENCY=true关闭并行场景,以及PLATFORM=linux/arm64指定平台。step 参考覆盖了从"给定容器环境变量"到"断言 PDF 页数、方向、内容、PDF/A 合规(verapdf校验,支持 PDF/A-1b/2b/3b、PDF/UA-1、PDF/UA-2)、加密、扁平化、嵌入式文件"等丰富断言。
Pull Request 规范
提交信息:Conventional Commits
提交信息遵循 Conventional Commits 格式:<type>(<scope>): <description>。
常用类型:feat、fix、refactor、test、docs、chore、ci、build。scope 对应改动所属的模块或领域,例如chromium、pdfengines、api。
提交时必须分阶段暂存具体文件(stage specific files),绝不使用git add -A或git add .。
提交前检查清单
打开 PR 之前,逐项确认:
- 无向后兼容性回归,见 Backward compatibility 一节;
- 满足代码规范:错误包装、日志、遥测、import 排序、无 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 参考、可用标签、编写新测试的方法;
.bruno/README.md:.bru文件格式、约定、路由更新检查清单;- 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
后端开发工具为 watermarks-remover 贡献代码:分层架构、测试门禁与协作规范实战指南
为 watermarks remover 贡献代码:分层架构、测试门禁与协作规范实战指南 watermarks remover 是一个以隐私为中心的开源项目:它
AI 技能人工智能内容安全Mirai Console 开发与贡献指南:模块架构、构建流程与代码规范
Mirai Console 开发与贡献指南:模块架构、构建流程与代码规范 Mirai Console 是 mirai 生态中高效率 QQ 机器人框架(基于 mi
即时通讯
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考