Skip to content

HTML to PDF in Go: the libraries that work, and their catches

Go has no built in HTML to PDF, and half the packages people find are archived. Here is the current map: chromedp for real Chromium output, why gofpdf and the wkhtmltopdf wrappers are dead ends, and when to reach for Gotenberg or a hosted API instead, with code.

Go is a wonderful language for building services and a frustrating one for making PDFs from HTML. There is no standard library answer, half the packages a search turns up were archived years ago, and the one that renders like a real browser also happens to be the one that eats your server. So before you paste in the first snippet you find, here is the current, honest map of HTML to PDF in Go: what actually renders modern CSS, what to avoid, and where a hosted service earns its keep.

The short version

For output that matches a real browser, use chromedp, which drives headless Chrome from Go over the DevTools protocol and prints the page to PDF. Do not build new work on gofpdf (archived in 2021) or on the wkhtmltopdf wrappers (the binary was archived in 2023 with unpatched CVEs), and treat gofpdf as a document builder rather than an HTML renderer anyway. If you want browser fidelity without hosting Chrome inside your Go binary, run Gotenberg as a separate service or call a hosted rendering API. The decision comes down to one question: does a browser belong in your infrastructure, and if so, do you want to be the one operating it?

chromedp: real Chromium, driven from Go

chromedp is the closest thing Go has to a first-class HTML to PDF path. It speaks the Chrome DevTools Protocol directly, with no Node in the middle, so you launch or attach to a Chromium instance, navigate to a URL or set the page content, and call Page.printToPDF. Because a current browser does the rendering, flexbox, CSS grid, web fonts and JavaScript all work, and the PDF matches what a user sees in Chrome.

ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()

var buf []byte
err := chromedp.Run(ctx,
    chromedp.Navigate("https://app.example.com/invoices/8842"),
    chromedp.WaitVisible("#invoice-total", chromedp.ByID),
    chromedp.ActionFunc(func(ctx context.Context) error {
        var e error
        buf, _, e = page.PrintToPDF().WithPrintBackground(true).Do(ctx)
        return e
    }),
)
if err != nil { log.Fatal(err) }
os.WriteFile("invoice.pdf", buf, 0644)

The WaitVisible step matters more than it looks. Without it, chromedp will happily print the page before your chart or late data has rendered, leaving an empty box where the revenue graph should be. Set WithPrintBackground(true) or Chromium strips your background colors and images in print mode, which is the single most common reason a chromedp PDF comes out looking washed out.

The real cost of chromedp is operational, not code. You are now running Chrome next to your Go service, and teams consistently report that chromedp consumes a lot of server capacity under high-volume PDF generation: each render is a browser tab, memory climbs, and you need to manage a pool of contexts, reap dead browsers, and cap concurrency. Go itself is lean; the browser you bolt onto it is not. If you are pushing serious volume, it is worth putting a number on the extra CPU and memory before you commit, because a fleet of headless Chrome instances can quietly become one of the larger lines when you actually audit your cloud and SaaS spend.

Why gofpdf and the wkhtmltopdf wrappers are dead ends

Two families of package dominate old Stack Overflow answers, and both are traps for new code in 2026.

gofpdf is a well-loved pure Go library, but it does not convert HTML at all. It is a low-level builder: you position text, tables, lines and images yourself, cell by cell, the same shape as Prawn in Ruby or FPDF in PHP. It is excellent for a highly structured, fixed-format document like a shipping label or a boarding pass, where you want no browser and total control. But the flagship jung-kurt/gofpdf repository was archived in 2021, so it receives no updates, and if your goal is to render an HTML template it is simply the wrong tool. Reach for it only when there is no HTML in the first place.

go-wkhtmltopdf and similar wrappers shell out to the wkhtmltopdf binary, and that binary is finished. wkhtmltopdf was formally archived by its maintainer in early 2023: no more releases, several CVEs that will never be patched, including a high-severity SSRF flaw, and a rendering engine frozen on a years-old WebKit fork whose CSS support stopped advancing around 2014. Flexbox is unreliable, grid is absent, and modern fonts are hit or miss. If you have a legacy Go service that already uses it and the output looks right, there is no emergency, but do not build new features on it. The full case and the safe replacements are on the wkhtmltopdf alternative page.

Gotenberg: keep the browser, but out of your Go binary

If you like chromedp's output but not the idea of Chrome living inside your Go process, Gotenberg is the popular middle path. It is an open-source Docker service that wraps Chromium (and LibreOffice for Office files) behind a clean HTTP API. Your Go code sends a multipart request with your HTML or a URL and gets a PDF back, so the browser runs in its own container that you can scale, restart and memory-limit independently of your application. That separation is the whole point: a crashing render takes down a Gotenberg pod, not your API server.

The tradeoff is that you are still operating the browser, just at arm's length. You run and scale the Gotenberg containers, and a single Chromium instance handles only a handful of parallel renders before you need more replicas behind Kubernetes or ECS. It is a genuinely good option for a team that is comfortable running infrastructure and wants to keep everything inside its own network. The Gotenberg alternative page walks through the self-host versus hosted decision in detail.

A hosted API: no browser in your infrastructure at all

The last option removes the browser entirely. A hosted rendering API runs managed Chromium on someone else's servers, so from Go it is a single HTTP request with the standard library, no chromedp, no Chrome to install, no pool to tune, and it works on platforms where you cannot run a browser at all, like many managed and serverless hosts.

form := url.Values{}
form.Set("url", "https://app.example.com/invoices/8842")
form.Set("format", "Letter")
form.Set("archive", "true") // keep a timestamped copy of this render

req, _ := http.NewRequest("POST", "https://api.sitepdf.com/v1/render",
    strings.NewReader(form.Encode()))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SITEPDF_KEY"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

resp, err := http.DefaultClient.Do(req)

That archive flag is the capability none of the libraries offer. It stores a timestamped, immutable snapshot of exactly what was rendered, retrievable later by id, so you can prove months from now what a given invoice or statement looked like when you produced it. For finance and compliance heavy Go services, that provenance is often the reason to render through an API instead of a local library. The PDF generator API page has the full call shape, and if you are weighing running your own headless Chrome, the same trade-off from the Node side is broken down in the HTML to PDF in Node.js guide.

Which should you pick

SituationBest fit
Output must match Chrome, and a browser is welcome in your processchromedp
Browser fidelity, but you want Chrome in its own containerGotenberg (self-hosted)
Browser fidelity with no browser in your infra, plus a dated archiveHosted rendering API
No HTML, a structured document you lay out in codegofpdf (accept it is archived)
Legacy service on wkhtmltopdf that already worksLeave it, but plan the migration

Go does not give you an easy HTML to PDF path, and the packages that top a quick search are mostly archived. For anything with real CSS, the answer is a real browser, and the only meaningful question is who runs it. chromedp puts Chrome in your process, Gotenberg puts it in a container you operate, and a hosted API puts it on someone else's servers with a dated record thrown in. Decide the browser question first, and the rest of the choice makes itself.

Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.

§ Early access

Get on the early-access list

The API opens to the list first, in order. Early access locks the planned launch rates for 12 months. No card required, launching soon.

Render + archive, one API