Генерация PDF в Go: рабочие подходы и их цена
Три способа собрать PDF в Go — от рисования по координатам до рендеринга HTML через headless-браузер. У каждого своя цена по скорости, точности вёрстки и зависимостям.

Задача звучит просто: сформировать счёт, договор или отчёт в PDF по данным из базы. На практике вы упираетесь в развилку. Либо вы рисуете документ по координатам и получаете полный контроль, но переписываете макет при каждой правке. Либо берёте HTML-шаблон и тащите в контейнер целый браузер. Кириллица в первом же примере превращается в квадраты, потому что встроенные шрифты PDF её не знают. Разберём три подхода, чтобы вы выбрали нужный до того, как напишете первую строку, а не после.
Три способа и что вы платите за каждый
Все библиотеки для Go делятся по тому, как они описывают документ.
- Рисование по координатам — вы задаёте позиции текста, линий, прямоугольников в миллиметрах или пунктах. Библиотека собирает PDF напрямую, без промежуточного рендера. Быстро, без внешних зависимостей, но табличную вёрстку и переносы вы считаете руками.
- Разметочная модель — вы описываете блоки, отступы, таблицы, а библиотека раскладывает их сама. Ближе к вёрстке, но каждый движок имеет свой набор возможностей и свои ограничения.
- HTML → PDF через headless-браузер — вы отдаёте готовый HTML/CSS, Chromium рендерит его в PDF. Идеально повторяет веб-макет, но тащит за собой сотни мегабайт браузера и процесс, который надо контролировать.
Выбор движка PDF — это выбор, где будет жить сложность вёрстки: в вашем коде на Go или во внешнем процессе. Третьего не дано.
Подход 1: рисование по координатам (gofpdf / fpdf)
Классика — библиотека github.com/jung-kurt/gofpdf. Она заморожена (архивирована автором), поэтому для новых проектов чаще берут форк github.com/go-pdf/fpdf с той же логикой. Обе рисуют текст в заданной точке и не умеют автоматически верстать сложные блоки.
Главная ловушка новичка — кириллица. Встроенные в PDF шрифты (Helvetica, Arial) поддерживают только Latin-1. Чтобы вывести русский текст, нужно подключить TrueType-шрифт с UTF-8.
// go-pdf/fpdf, проверено на версии v0.9.0
package main
import (
"github.com/go-pdf/fpdf"
)
func main() {
pdf := fpdf.New("P", "mm", "A4", "")
// Подключаем шрифт с кириллицей. Файл DejaVuSans.ttf
// должен лежать рядом; utf-8 обязателен третьим аргументом
pdf.AddUTF8Font("DejaVu", "", "DejaVuSans.ttf")
pdf.SetFont("DejaVu", "", 14)
pdf.AddPage()
pdf.Cell(0, 10, "Счёт № 42 от 15 января")
// Сохраняем результат
if err := pdf.OutputFileAndClose("invoice.pdf"); err != nil {
panic(err)
}
}
Ключевой момент — именно AddUTF8Font, а не AddFont. Второй ждёт шрифт в формате внутренних метрик и с кириллицей вам не поможет. Файл шрифта нужно распространять вместе с приложением или встраивать через embed, иначе на сервере без него получите пустой текст.
Цена: таблицу из счёта вы собираете вручную — считаете ширину колонок, ловите переносы длинных наименований, сами рисуете границы ячеек. Для документа с фиксированным макетом это терпимо. Для отчёта, где число строк заранее неизвестно, — головная боль.
Подход 2: разметочная модель (maroto)
Если координаты утомляют, есть github.com/johnfercher/maroto — обёртка над fpdf с сеточной моделью: строки, колонки, автоматические отступы. Вы описываете документ блоками, а не точками.
Плюс — таблицы и колонки собираются на порядок быстрее. Минус — вы ограничены тем, что умеет сама maroto: сложная типографика, произвольные шрифтовые начертания и точное позиционирование даются хуже, чем в чистом fpdf. Для типовых счетов, накладных и актов этого хватает; для дизайнерского макета — нет.
Библиотека активно развивается, API v2 несовместим с v1. Перед использованием сверьте актуальный интерфейс с README репозитория — здесь не привожу код, потому что имена методов между мажорными версиями менялись, и правдоподобный, но устаревший вызов только собьёт вас.
Подход 3: HTML → PDF через Chromium
Когда документ уже существует как веб-страница и его надо повторить пиксель в пиксель, самый честный путь — отдать HTML в headless-Chromium. В Go для управления браузером используют github.com/chromedp/chromedp, который говорит с Chrome по протоколу DevTools.
// chromedp, проверено на версии v0.9.x
// Требует установленного Chrome или Chromium в системе
package main
import (
"context"
"os"
"time"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
// Ограничиваем время — зависший рендер не должен вешать сервис
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var pdfBuf []byte
err := chromedp.Run(ctx,
chromedp.Navigate("file:///tmp/invoice.html"),
chromedp.ActionFunc(func(ctx context.Context) error {
// PrintToPDF отдаёт готовый документ байтами
buf, _, err := page.PrintToPDF().
WithPrintBackground(true).
Do(ctx)
pdfBuf = buf
return err
}),
)
if err != nil {
panic(err)
}
if err := os.WriteFile("invoice.pdf", pdfBuf, 0o644); err != nil {
panic(err)
}
}
Кириллица здесь работает из коробки, если она есть в HTML и в системных шрифтах контейнера. Вёрстка — обычный CSS, включая @page для полей и разрывов страниц. Расплата серьёзная: в Docker-образ надо ставить Chromium со всеми его зависимостями, следить за зомби-процессами и памятью. Один запущенный Chrome под нагрузкой съедает сотни мегабайт, а десяток параллельных рендеров кладёт сервис.
Когда что выбирать
| Критерий | fpdf | maroto | chromedp |
|---|---|---|---|
| Внешние зависимости | Нет | Нет | Chromium в системе |
| Точность сложной вёрстки | Ручная | Средняя | Как в браузере |
| Скорость на документ | Миллисекунды | Миллисекунды | Сотни мс — секунды |
| Память под нагрузкой | Низкая | Низкая | Высокая |
| Кириллица | Через UTF8-шрифт | Через UTF8-шрифт | Из коробки |
Правило простое. Фиксированный бланк с предсказуемой структурой (счёт, договор по шаблону) — fpdf. Таблицы и типовые отчёты без дизайнерских изысков — maroto. Документ, который обязан повторять существующую веб-страницу, и вы готовы платить инфраструктурой — chromedp.
Что ломается на проде
- Пустой или квадратный текст. Не подключён UTF8-шрифт (fpdf/maroto) либо в контейнере нет системных шрифтов с кириллицей (chromedp). Ставьте пакет шрифтов в образ или встраивайте TTF через
embed. - Файл шрифта не найден на сервере. Локально всё работает, на проде — паника. Кладите шрифт рядом с бинарником или вшивайте в него, не полагайтесь на путь из разработки.
- Chromium висит и не отдаёт управление. Всегда задавайте
context.WithTimeoutи убивайте контекст вdefer. Без таймаута один битый HTML заблокирует горутину навсегда. - Утечка процессов Chrome. Каждый забытый
cancel()оставляет висящий процесс браузера. Под нагрузкой они съедают память сервера за минуты. - Гонка при параллельной генерации. Один экземпляр pdf-объекта fpdf не потокобезопасен. Создавайте новый на каждый запрос, не переиспользуйте.
Что сделать дальше
Определитесь с подходом до кода — переезд с fpdf на chromedp в середине проекта означает переписать всю генерацию. Если выбрали браузер, сразу заложите пул воркеров с ограничением на число параллельных рендеров и метрику времени генерации: под нагрузкой именно она деградирует первой. Инвалидация и кеширование готовых PDF — отдельная тема, здесь мы рассмотрели только сборку документа. Дальше смотрите на размер выходных файлов: встроенный TTF-шрифт добавляет к каждому PDF свой вес, и подмножество глифов (subsetting) стоит проверить, если документов много.
Карточки для маркетплейсов: Таблица товаров и папка фотографий на входе — комплект слайдов и описаний на выходе, по требованиям Wildberries и Ozon.
Спросить за 5 ₽Источники: go-pdf/fpdf — репозиторий на GitHub, chromedp — репозиторий на GitHub
Частые вопросы
А если у меня старый gofpdf, а не форк go-pdf/fpdf?
Кириллица всё равно выводится квадратами, что делать?
Можно ли генерировать PDF без внешних зависимостей вообще?
Сколько параллельных рендеров через chromedp выдержит сервер?
Как встроить шрифт в бинарник, чтобы не таскать файл?
Подходит ли chromedp для генерации тысяч документов пачкой?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.