全部文章

go-pdf/fpdf 也归档了。2026 年的 Go PDF 栈长这样。

jung-kurt/gofpdf 2021 年归档,go-pdf/fpdf 2025 年跟进。2026 年我们实际在用的 Go PDF 栈是 gpdf — 原因、权衡与迁移路径。

作者: gpdf team

TL;DR

fpdf 系两个还在维护的分支都已经只读。jung-kurt/gofpdf2021 年 9 月 归档;社区 fork go-pdf/fpdf2025 年 归档。不会再有"下一任维护者"。新项目的现代默认是 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 图里的库,它其实意味着四件很具体的事:

  1. 不会有安全补丁。TTF 解析器里出现内存安全问题,不会有人合并修复到上游。你可以自己 fork 来修,大多数团队不会。
  2. 不会跟进 Go 工具链。Go 1.25 的循环变量语义今天在 gofpdf 上跑得好好的。但明天 for range 或某个标准库 API 废弃之类的改动如果把它弄坏,修补只读仓库的 fork 就是你自己的事。
  3. 不会跟进规范。PDF 2.0(ISO 32000-2)在 2020 年就定稿了。gofpdf 大部分实现停留在 PDF 1.7。页面级关联文件、丰富的 XMP 元数据、现代数字签名(PAdES-B-LT),要么缺失,要么靠第三方胶水拼上去。
  4. 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/gofpdf2021 已归档MIT后加的原版。在大多数语言的搜索结果里至今仍是第一。
go-pdf/fpdf2025 已归档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,无 CGOgo 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 或数字签名的可选扩展会带少量依赖,但都是选择性的。)
  • 原生 CJKWithFont 在构建 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。

基准gpdfgofpdfgopdfMaroto v2
单页 (hello)13 µs132 µs423 µs237 µs
4×10 明细表108 µs241 µs835 µs8.6 ms
100 页报告683 µs11.7 ms8.6 ms19.8 ms
复杂 CJK 发票133 µs254 µs997 µs10.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/gopdfunidoc;图表密集的团队会把图表预渲染成 PNG 再嵌入。这里"栈"指的是一张短清单,不是分层架构。

迁移期间能把 gpdf 和 go-pdf/fpdf 并存吗? 可以。import 路径和类型都不一样。新接口走 gpdf,旧接口留在 go-pdf/fpdf,等有时间再重写。运行时没有冲突。

会有 go-pdf/fpdf v3 或新 fork 吗? 也许。gpdf 的赌注不是"那个 fork 永远不会 unarchive"——而是架构跟不上现在要造的东西。新 fork 如果不改布局模型,会继承同样的限制;如果改了,那它就更接近 gpdf 而不是 fpdf。

signintech/gopdf 作为现代替代怎么样? 真的在维护、真的零依赖。API 是坐标级的——SetXSetYCellWithOption——适合表单叠加和固定模板。对于带表格和重复页眉页脚的发票类文档,你最后还是得在上面写一层布局辅助,掉进 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

⭐ 在 GitHub 加星 · 查看文档

延伸阅读