Gere uma fatura em PDF em Go em menos de 50 linhas
Código completo e executável para gerar uma fatura PDF em Go — 50 linhas com gpdf, zero dependências, sem Chromium, sem CGO. Inclui cabeçalho, tabela e total.
TL;DR
Uma fatura PDF funcional em Go, de ponta a ponta, em 50 linhas. Um main.go, um go get, sem Chromium, sem CGO, sem linguagem de template, sem HTML. Tabela, linhas com zebrado, totais alinhados à direita. Roda. O código está abaixo; o resto do post explica o que cada bloco faz e onde o padrão deixa de escalar.
Se quiser ler o código primeiro:
go get github.com/gpdf-dev/gpdf
E cole o main.go da próxima seção.
Por que "menos de 50 linhas" é o limite que importa
A razão honesta pela qual este post existe: a maioria das pessoas que pesquisa "gerar fatura pdf em go" encontra artigos que (a) recomendam subir Chromium headless, ou (b) mostram 400 linhas de operadores PDF de baixo nível para renderizar uma única tabela. Tecnicamente as duas estão certas. Nenhuma é o formato que a tarefa tem.
Uma fatura razoável tem:
- Cabeçalho com sua empresa e a do cliente
- Número da fatura e data de vencimento
- Tabela de itens
- Um total
Quatro coisas. Deveriam ser quatro blocos de código. Se não cabe em uma tela, a biblioteca foi escolhida errada.
50 linhas é mais ou menos o limite em que o código ainda cabe em uma tela de editor normal. É também o patamar em que um revisor lê do começo ao fim em vez de pular para os testes. Atingir esse patamar significa que você pode colar o resultado numa mensagem de Slack e alguém aprende a biblioteca só por aquela mensagem.
O código abaixo está formatado com gofmt, imports totalmente expandidos, todos os caminhos de erro tratados. Sem truques, sem pacote helper escondido. O que você vê é o que compila.
As 50 linhas
package main
import (
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/pdf"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(template.WithPageSize(document.A4))
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Text("ACME Ltda.", template.FontSize(22), template.Bold())
c.Text("Av. Paulista 1000, São Paulo 01310")
})
r.Col(6, func(c *template.ColBuilder) {
c.Text("FATURA #INV-2026-001", template.Bold(), template.AlignRight())
c.Text("Vencimento: 2026-03-31", template.AlignRight())
})
})
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Spacer(document.Mm(6))
c.Table(
[]string{"Descrição", "Qtd.", "Valor unit.", "Valor"},
[][]string{
{"Desenvolvimento frontend", "40 h", "R$ 750,00", "R$ 30.000,00"},
{"Desenvolvimento backend", "60 h", "R$ 750,00", "R$ 45.000,00"},
{"Design UI/UX", "20 h", "R$ 600,00", "R$ 12.000,00"},
},
template.ColumnWidths(40, 15, 20, 25),
template.TableHeaderStyle(template.Bold(), template.BgColor(pdf.RGBHex(0xF0F0F0))),
template.TableStripe(pdf.RGBHex(0xFAFAFA)),
)
c.Text("Total: R$ 87.000,00", template.AlignRight(), template.Bold(), template.FontSize(14))
})
})
b, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("invoice.pdf", b, 0644); err != nil {
log.Fatal(err)
}
}
go run . produz invoice.pdf no diretório atual. Em um M1 o programa inteiro termina em alguns milissegundos — a geração do PDF em si está abaixo de 150 µs; o resto é inicialização de processo.
O que cada bloco faz
Imports
Quatro pacotes do gpdf:
github.com/gpdf-dev/gpdf— a fachada. Usamos apenasgpdf.NewDocument, que é um wrapper fino sobretemplate.New.github.com/gpdf-dev/gpdf/document— unidades (Mm,Pt,Cm,In,Em,Pct), tamanhos de página (A4,Letter,Legal), margens.github.com/gpdf-dev/gpdf/pdf— primitivas de cor (RGBHex,Gray, constantes comopdf.White).github.com/gpdf-dev/gpdf/template— o Builder API.
Zero dependências externas. Após go get github.com/gpdf-dev/gpdf, o require do go.mod tem uma linha.
Construção do documento
doc := gpdf.NewDocument(template.WithPageSize(document.A4))
gpdf.NewDocument aceita ...template.Option. Tamanho de página, margens, fonte padrão, metadados, fontes custom: tudo é opção WithXxx. Margem padrão 20 mm.
A linha do cabeçalho
page.AutoRow(func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) { ... })
r.Col(6, func(c *template.ColBuilder) { ... })
})
O gpdf usa uma grade de 12 colunas, mesmo modelo mental do Bootstrap. Uma linha tem 12 unidades horizontais; r.Col(6, ...) ocupa metade; dois Col(6) preenchem a linha.
AutoRow significa que a altura da linha é a do conteúdo mais alto. Dentro de cada coluna você empilha c.Text(...) de cima para baixo. Sem posicionamento explícito — o builder controla um cursor internamente.
A coluna direita usa template.AlignRight(). Opções de texto são componíveis: c.Text("FATURA", template.Bold(), template.AlignRight(), template.FontSize(20)) empilha três modificadores em uma única chamada. A ordem não importa.
A tabela de itens
c.Table(
[]string{"Descrição", "Qtd.", "Valor unit.", "Valor"},
[][]string{ /* linhas */ },
template.ColumnWidths(40, 15, 20, 25),
template.TableHeaderStyle(template.Bold(), template.BgColor(pdf.RGBHex(0xF0F0F0))),
template.TableStripe(pdf.RGBHex(0xFAFAFA)),
)
ColumnWidths são porcentagens da coluna pai, não pontos absolutos. Os quatro valores devem somar 100. Se não somar, não há erro mas a última coluna transborda — a única pegadinha.
TableHeaderStyle aceita as mesmas opções de texto usadas em outros lugares. TableStripe(color) alterna o fundo das linhas. A tabela mede cada célula, escolhe a altura pela mais alta e, se passar da página, o gpdf quebra e redesenha o cabeçalho na continuação.
O total
c.Text("Total: R$ 87.000,00", template.AlignRight(), template.Bold(), template.FontSize(14))
Outro Text fora da tabela, alinhado à direita, um pouco maior. Se quiser mais respiro, adicione c.Spacer(document.Mm(3)) antes.
Gerar e escrever
b, err := doc.Generate()
if err != nil { log.Fatal(err) }
if err := os.WriteFile("invoice.pdf", b, 0644); err != nil { log.Fatal(err) }
doc.Generate() devolve ([]byte, error). Não toca o sistema de arquivos. O slice é um PDF completo — grave no disco, envie para S3, devolva como resposta HTTP com w.Write(b), anexe em e-mail. Se preferir streaming, use doc.Render(w io.Writer).
Deixando mais bonita sem passar de 50 linhas
Cor da marca. Escolha um hex (por exemplo, azul-marinho 0x1A237E) e aplique no nome da empresa e no cabeçalho da tabela:
brand := pdf.RGBHex(0x1A237E)
c.Text("ACME Ltda.", template.FontSize(22), template.Bold(), template.TextColor(brand))
template.TableHeaderStyle(template.Bold(), template.TextColor(pdf.White), template.BgColor(brand)),
Subtotal e impostos. Acima do total, três c.Text empilhados:
c.Text("Subtotal: R$ 87.000,00", template.AlignRight())
c.Text("ISS (5%): R$ 4.350,00", template.AlignRight())
c.Text("Total: R$ 91.350,00", template.AlignRight(), template.Bold(), template.FontSize(14))
Requisitos de NFe/DANFE (Brasil). Para uma Nota Fiscal eletrônica, os campos obrigatórios (CNPJ do emitente, natureza da operação, chave de acesso) são apenas c.Text adicionais. O layout não muda — a diferença está nos dados e na assinatura digital posterior, não na montagem do documento.
Uma linha sobre o total. Entre o subtotal e o total:
c.Spacer(document.Mm(2))
c.Line(template.LineThickness(document.Pt(0.5)))
c.Spacer(document.Mm(2))
Executando
mkdir invoice-demo
cd invoice-demo
go mod init example.com/invoice-demo
go get github.com/gpdf-dev/gpdf
# cole main.go
go run .
open invoice.pdf # macOS; xdg-open no Linux, start no Windows
Quando esse padrão deixa de servir
- Os itens viram dados. Quando vêm de uma query ou JSON, a tabela não muda — você só constrói
[][]stringa partir dos dados. - Você quer reusar o layout. Assim que começar a gerar faturas em loop, extraia o corpo para
func renderInvoice(doc *template.Document, inv Invoice). - O layout ganha branches. Com condicionais o Builder API fica verboso — o entrypoint JSON ou o entrypoint Go templates se encaixa melhor.
- Você precisa de CJK. Japonês, chinês, coreano aparecem como quadrados vazios com a fonte padrão. Registre um TTF com
template.WithFont. Veja Como embutir uma fonte japonesa no gpdf.
Nenhum dos quatro é "reescrever do zero" — são extensões incrementais.
FAQ
Posso usar em faturas comerciais sem atribuição? Sim. O gpdf é MIT. Pode construir em cima o que quiser, inclusive produtos comerciais fechados. Atribuição não é obrigatória (uma star no GitHub é bem-vinda).
Dá para escrever direto em io.Writer sem o slice?
Sim — doc.Render(w io.Writer) error. A versão com doc.Generate() é uma comodidade para o caso comum de querer []byte.
Qual é a velocidade real? As 50 linhas acima geram o PDF em cerca de 100 µs num M1. Um hello world de uma página fica em 13 µs. Em cargas batch — geração de faturas noturna, relatórios em massa — o gpdf raramente é o gargalo.
Dá para gerar uma fatura PDF em Go sem o gpdf?
Dá. jung-kurt/gofpdf funciona (arquivado mas estável), signintech/gopdf num nível mais baixo, e johnfercher/maroto com outra abstração de layout. Todos ficam mais verbosos que as 50 linhas acima para a mesma fatura.
Por que não existe um helper gpdf.Invoice?
Porque "fatura" significa coisas diferentes em países diferentes e qualquer simplificação deixa alguém de fora. Preferimos te dar um ponto de partida de 50 linhas do que um NewInvoice(...) que quebra na primeira NFe brasileira ou 適格請求書 japonês.
O PDF valida como PDF/A?
Por padrão sai PDF 1.7 normal. Para PDF/A-2b passe gpdf.WithPDFA(pdfa.Level2B) ao construir o documento. Ver Construindo PDF/A-2b em Go puro.
Experimente o gpdf
O gpdf é uma biblioteca Go para gerar PDFs. MIT, zero dependências, CJK nativo, 10–30× mais rápida que alternativas nos workloads que medimos.
go get github.com/gpdf-dev/gpdf
⭐ Star no GitHub · Ler os docs
Próximas leituras
- Por que o gpdf é 10–30× mais rápido que outras libs Go de PDF — os números por trás do "algumas centenas de microssegundos".
- Go PDF Library Showdown 2026 — quantas linhas a mesma fatura ocupa em gofpdf, gopdf e Maroto.
- Como a grade de 12 colunas do gpdf funciona — o modelo de layout usado no cabeçalho acima em mais detalhes.