go-pdf/fpdf 也归档了。2026 年的 Go PDF 栈长这样。
jung-kurt/gofpdf 2021 年归档,go-pdf/fpdf 2025 年跟进。2026 年我们实际在用的 Go PDF 栈是 gpdf — 原因、权衡与迁移路径。
TL;DR
fpdf 系两个还在维护的分支都已经只读。jung-kurt/gofpdf 于 2021 年 9 月 归档;社区 fork go-pdf/fpdf 于 2025 年 归档。不会再有"下一任维护者"。新项目的现代默认是 gpdf:纯 Go、零外部依赖、CJK 原生、在常见工作负载上快 10–30 倍。这篇文章是 2026 年的局势图,以及对 "何时选 gpdf、何时不选" 的诚实回答。
现状
上周同事敲下 go get github.com/go-pdf/fpdf,在 GitHub 的横幅前停住:"This repository has been archived by the owner. It is now read-only." —— 这本来应该是 修复版,是 2021 年被归档的 jung-kurt/gofpdf 的社区接棒 fork。
它也归档了。README 现在建议去找别的库。
过去五年里,如果你用 Go 写服务、输出 PDF(发票、报表、运单、增值税发票、电子归档文件),go.mod 最底下那一行几乎一定是这两个之一。Stack Overflow 的回答指向 jung-kurt/gofpdf;更新一点的教程指向 go-pdf/fpdf。现在两个都是供应链上的负债 —— CVE 分诊、Go 版本跟进、性能修复、规范更新,全部冻结。
这篇不是又一份逐行迁移指南——那份我们已经写过了。这篇想回答迁移指南没回答的问题:2026 年用 Go 生成 PDF,到底应该选什么?生态为什么会走到这一步?
"归档" 在生产中意味着什么
GitHub 的 "archived" 标签看起来温柔。对于一个在你 import 图里的库,它其实意味着四件很具体的事:
- 不会有安全补丁。TTF 解析器里出现内存安全问题,不会有人合并修复到上游。你可以自己 fork 来修,大多数团队不会。
- 不会跟进 Go 工具链。Go 1.25 的循环变量语义今天在 gofpdf 上跑得好好的。但明天
for range或某个标准库 API 废弃之类的改动如果把它弄坏,修补只读仓库的 fork 就是你自己的事。 - 不会跟进规范。PDF 2.0(ISO 32000-2)在 2020 年就定稿了。gofpdf 大部分实现停留在 PDF 1.7。页面级关联文件、丰富的 XMP 元数据、现代数字签名(PAdES-B-LT),要么缺失,要么靠第三方胶水拼上去。
- CJK 不会再前进。gofpdf 的 Unicode 路径是在 "单字节字体" 的旧设计上后加的。能跑,但在大多数真实配置下会嵌入完整字体而不是子集;某些 CJK TTF 上会触发 glyph-id 冲突,输出乱码。
go-pdf/fpdf继承了同样的架构。
安全和前向兼容两条,是合规会议上最刺眼的。"我们的 PDF 库已归档且无人发 CVE 补丁",不是审计想听的回答。如果你的 PDF 还涉及电子会计档案或增值税电子发票的保存期,这个问题不能再拖。
为什么两个 fork 都死了
把归档简单归咎于维护者倦怠很容易——一位累了的 reviewer,一个 bus factor 为 1 的维护人下线。那是原因之一,但不是全貌。架构让追赶变得困难。
jung-kurt/gofpdf 是 FPDF 的移植——一个 2002 年的 PHP 库。PHP 原版通过在页面上推游标、按流程吐内容:SetXY(x, y)、Cell(w, h, text)、Ln(h)。这个模型在 2002 年的 PHP 下是合理折中——当时的替代方案是裸 PostScript 或商业工具包。移植到 Go 时,保留了游标、保留了单字节字体表、保留了手动分页管理。
每过一年,"人们想生成的东西"和"游标模型能表达的东西"之间的落差就变大一点。发票是表格。报告是带重复页眉页脚的网格。运单是二维码 + 本地语言文本。游标被辅助函数包起来,辅助函数又被教程包起来,到 2023 年左右,大多数人"针对 gofpdf 写的代码"其实不是 gofpdf——是每个团队自己的胶水层,试图把游标装成布局引擎。
go-pdf/fpdf 继承了这一切。这个 fork 重构了内部实现、修了一些老 bug,但没法改公开 API 的形状——一动就要让所有下游工程崩。库的外形冻结在 2002 年的 PHP 里,维护这个形状的成本比收益涨得更快。
所以:两位维护者,两次归档,一个架构上的原因。2026 年要重新来一遍,就得选一个匹配今天 PDF 生成方式的方法——今天的方式更像搭网页,不像驱动绘图仪。
2026 年 Go PDF 的局势
在推荐任何东西之前,先把场面列出来。"maintained"(在维护)这里的意思是"最近 6 个月有提交、issue 有响应"。
| 库 | 状态 (2026-04) | 许可证 | CJK 原生 | 零依赖 | 备注 |
|---|---|---|---|---|---|
jung-kurt/gofpdf | 2021 已归档 | MIT | 后加的 | 是 | 原版。在大多数语言的搜索结果里至今仍是第一。 |
go-pdf/fpdf | 2025 已归档 | MIT | 后加的 | 是 | 上者的社区 fork。同样的架构,同样的天花板。 |
signintech/gopdf | 维护中 | MIT | 部分 | 是 | 低层库,你自己写坐标。适合表单叠加。 |
johnfercher/maroto v2 | 维护中 | MIT | 经 gofpdf | 否 | 网格优先的 builder,但底层用 go-pdf/fpdf。 |
unidoc/unipdf | 维护中 | 商业 | 是 | 否 | 功能齐全的 PDF SDK。商用需付费许可。 |
chromedp + Chromium | 维护中 | MIT + Chrome | 是 | 否——自带浏览器 | 用无头 Chrome 做 HTML→PDF。运行时巨大。 |
gpdf | 维护中 | MIT | 原生 | 是 | 纯 Go 重写。Builder API、12 列网格。 |
只看这张表就能得出几个结论:
目前"仍在维护"的每个方案,要么是商业许可、要么自带巨大运行时、要么坐在一个即将过时的地基上。例外是 signintech/gopdf——确实在维护、依赖也轻。但它是坐标级库,你又回到了换个包名继续写 SetXY 的老路。
Maroto v2 是个 API 不错的网格优先 builder。问题在于 go.mod 底下是 go-pdf/fpdf。fpdf 的性能天花板和 CJK 限制,就是 Maroto 的天花板。v3 可能摆脱它,但还没出来。
unipdf 功能丰富,但对商业项目不是 MIT 兼容。按席位或按部署收费。如果你的营收撑得起这个账单,它是合理选择;对 OSS 副业或早期创业公司,许可证数学算不过来。
chromedp 能跑,但你是在发布一个浏览器。100 MB 的基础镜像变成 1 GB+,Serverless 冷启动痛苦,字体还得单独装进容器。好处是可以复用 React 模板;坏处是为了出一张发票一直在跑 Chromium。
缺口很明显:一个纯 Go、零依赖、CJK 原生、网格优先、不用商业许可也不用浏览器运行时的库。gpdf 就是为此而生。
gpdf 是什么
gpdf(github.com/gpdf-dev/gpdf)是一次干净重写,不是 fork。PDF 线格式写入、布局引擎、TrueType 子集化器——全部用纯 Go 从零写。
对大多数团队最重要的三个属性:
- 纯 Go,无 CGO。
go build是静态的。GOOS=linux GOARCH=arm64 go build在 MacBook 上不配工具链就能过。Docker 镜像保持小——12 MB 的 distroless 容器跑得动。 - 零外部依赖。
go get github.com/gpdf-dev/gpdf之后go mod graph只有一行:gpdf 自己。核心只用标准库。(HTML→PDF 或数字签名的可选扩展会带少量依赖,但都是选择性的。) - 原生 CJK。
WithFont在构建 document 时注册一个 TrueType 字体。子集嵌入在渲染时自动发生。一张 200 字的中文/日文发票里嵌入的字体约 30 KB 子集,不是 5 MB 的完整字体。
API 形态是声明式的。你描述一棵行/列的树,布局引擎负责放置。网格是 12 列——Bootstrap 从 2011 年用到今天的同一个 idiom。写过哪怕一行 HTML/CSS,gpdf 的 API 都会让你觉得眼熟:
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {
c.Text("发票 #2026-0416", template.FontSize(18), template.Bold())
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("2026-04-16", template.AlignRight())
})
})
网格细节见 gpdf 的 12 列网格是怎么工作的?。一句话版本:Col(span, fn) 接受 1–12 的 span,span / 12 是该列在行宽中的比例。
最小的 go-pdf/fpdf → gpdf 差分
如果你是从 go-pdf/fpdf(而不是 jung-kurt/gofpdf)过来的,好消息:API 表面几乎一样——go-pdf/fpdf 在调用层几乎没改过什么。迁移到 gpdf 的步骤和 gofpdf 指南一致,从改一行 import 路径开始。
最小差分——一个"生成 PDF"的 HTTP handler:
Before — go-pdf/fpdf:
package main
import (
"net/http"
"github.com/go-pdf/fpdf"
)
func handler(w http.ResponseWriter, r *http.Request) {
pdf := fpdf.New("P", "mm", "A4", "")
pdf.AddPage()
pdf.SetFont("Arial", "B", 16)
pdf.Cell(40, 10, "Hello, World!")
w.Header().Set("Content-Type", "application/pdf")
if err := pdf.Output(w); err != nil {
http.Error(w, err.Error(), 500)
}
}
After — gpdf:
package main
import (
"net/http"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/template"
)
func handler(w http.ResponseWriter, r *http.Request) {
doc := gpdf.NewDocument(
gpdf.WithPageSize(document.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
)
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("Hello, World!", template.FontSize(16), template.Bold())
})
})
w.Header().Set("Content-Type", "application/pdf")
if err := doc.Render(w); err != nil {
http.Error(w, err.Error(), 500)
}
}
三行游标代码变成三次 builder 调用。结构直接呈现在源码里——不再藏在 Cell 调用顺序中。CJK 只需再加 gpdf.WithFont("NotoSansJP", ttfBytes) ——不需要 AddUTF8Font,不需要文件系统路径,不需要 UTF-8 标志。完整流程见 如何在 gpdf 中嵌入日文字体?。
gofpdf 迁移指南 中还有 5 组 before/after:表格、重复页眉页脚、页码、绝对定位。那些内容对 go-pdf/fpdf 用户一字不差地适用——只需改 import 路径。
基准数据
"快"很容易说出口,很难说清楚。下表来自 gpdf/_benchmark/benchmark_test.go,在 Apple M1、Go 1.25 上测得。工作负载是生产代码真正会做的——不是专门挑来讨好某个库的 micro-benchmark。
| 基准 | gpdf | gofpdf | gopdf | Maroto v2 |
|---|---|---|---|---|
| 单页 (hello) | 13 µs | 132 µs | 423 µs | 237 µs |
| 4×10 明细表 | 108 µs | 241 µs | 835 µs | 8.6 ms |
| 100 页报告 | 683 µs | 11.7 ms | 8.6 ms | 19.8 ms |
| 复杂 CJK 发票 | 133 µs | 254 µs | 997 µs | 10.4 ms |
单页 13 µs 意味着单核每秒可以产出约 75,000 份 hello-world PDF;108 µs 的发票意味着每秒约 9,000 份。重点不是跑分自吹——而是你可以不再纠结"PDF 生成要不要缓存、要不要塞进异步队列"。对大多数工作负载,请求路径上直接生成就够了。
表格基准里 Maroto v2 之所以慢,是因为它底层驱动 go-pdf/fpdf 又在上面加了一层自己的布局。这不是在批评 Maroto 的 API——API 是好的——而是坐在 fpdf 地基上的结构性成本。等 Maroto v3 甩掉 fpdf 依赖时,这一列的数字会变。
100 页基准值得多说两句。gpdf 的流式写入会随着行的布局过程同时输出内容;gofpdf 每页缓冲更多状态。对分页密集的工作负载(月度报告、目录、合规导出),在文档体量上限处差距是"分钟 vs 秒"的数量级。
什么时候 不该 选 gpdf
迁移类文章必须诚实回答 "什么时候不迁":
- AcroForm / 可填写表单。如果你要生成让用户在 Acrobat 里填写的 PDF,gpdf 的表单字段支持还很少。
unidoc在这块更完整;signintech/gopdf有部分 AcroForm 支持。未来会补上,但今天是缺口。 - 任意矢量路径、复杂绘图。
c.Line()在列内画一条横线。如果你要贝塞尔曲线、自定义路径、渐变填充来画图表或技术图,gpdf 现在到不了。(预渲染的图表图片嵌入没问题——这里说的是绘图原语本身。) - 大量使用
SetXY的现成 gofpdf 代码库。如果你的代码是 2000 行游标操作,迁移更像重写而不是替换。重写后的代码几乎总是更短,但在 deadline 当天 "几乎" 是很冷的安慰。迁移指南 里有诚实的工作量估算。 - 现在就要全 CSS 支持的 HTML → PDF。gpdf 的
gpdf-pro扩展有 HTML 子集,但与 Chromium 的完整 CSS 对等不是目标。如果模板是复杂的 React 组件,chromedp或商业 API 更直接。
以上都不刺到你,gpdf 就是默认选择。有一项刺到了,正常的做法是两个库并存——新 PDF 用 gpdf,边缘场景留在原库,等 gpdf 追上后再迁。
合规视角
生态文章里比较少有人讲这个:归档的依赖会出现在 SOC 2 和 ISO 27001 的审计报告里。审计官想确认供应链里的三方代码在积极维护。"2021 年归档" 会触发 finding,"2025 年归档" 也会。"内部 fork" 会触发关于 0day 怎么打补丁的追问。
这就是为什么一些在大公司过安全评审的团队悄悄问我们,"gpdf 什么时候出稳定 v1"。答案是:已经出了。github.com/gpdf-dev/gpdf 有 semver tag,v1 API 面已冻结。项目有安全联系人、负责任披露政策,CI 会在 Go 1.22 到 1.26 全范围跑测试。
你不是为了过审才迁——是在审计开口要求之前先动。
FAQ
"现代 Go PDF 栈" 是 gpdf 一个库,还是多个库组合?
对大多数团队就是 gpdf 一个。单库覆盖文档生成、CJK、表格、网格、分页、输出。有可填写表单需求的团队会给那类文档单独搭上 signintech/gopdf 或 unidoc;图表密集的团队会把图表预渲染成 PNG 再嵌入。这里"栈"指的是一张短清单,不是分层架构。
迁移期间能把 gpdf 和 go-pdf/fpdf 并存吗? 可以。import 路径和类型都不一样。新接口走 gpdf,旧接口留在 go-pdf/fpdf,等有时间再重写。运行时没有冲突。
会有 go-pdf/fpdf v3 或新 fork 吗? 也许。gpdf 的赌注不是"那个 fork 永远不会 unarchive"——而是架构跟不上现在要造的东西。新 fork 如果不改布局模型,会继承同样的限制;如果改了,那它就更接近 gpdf 而不是 fpdf。
signintech/gopdf 作为现代替代怎么样?
真的在维护、真的零依赖。API 是坐标级的——SetX、SetY、CellWithOption——适合表单叠加和固定模板。对于带表格和重复页眉页脚的发票类文档,你最后还是得在上面写一层布局辅助,掉进 gofpdf 用户掉过的同一个坑。gpdf 和 gopdf 其实不真正竞争——解决的是相邻问题。
gpdf 有商业/托管版吗?gpdf-api 正在做——一个接收 JSON 模板、返回 PDF 的托管 API。还没公开。上线时这里会发文章。OSS 库会继续保持 MIT、零依赖、独立可用。
路线图的优先级? 2026-04 公开路线图:(1) AcroForm 表单字段;(2) 完整 PDF/A-3 合规;(3) gpdf-pro 的 HTML→PDF 覆盖扩展;(4) RTL 文本支持(阿拉伯语、希伯来语)。优先级反馈欢迎在 GitHub issue 里提。
试用 gpdf
gpdf 是一个 Go 语言的 PDF 生成库。MIT 许可、零外部依赖、原生 CJK 支持。
go get github.com/gpdf-dev/gpdf
延伸阅读
- gofpdf 已归档。如何迁移到 gpdf。 — 逐 API 对照
- Go PDF 库横评 2026 — 更深入的对比基准与特性表
- gpdf 的 12 列网格是怎么工作的? — 取代游标操作的 builder 习语
- 如何在 gpdf 中嵌入日文字体? — 不用
AddUTF8Font的 CJK