How to Generate PDFs in Go with LightningPDF - Complete Tutorial

Learn how to generate PDFs in Go using LightningPDF API. Covers HTML to PDF, templates, batch generation, error handling, and best practices.

By LightningPDF Team · · 3 min read
How to Generate PDFs in Go with LightningPDF - Complete Tutorial
TL;DR: Generate production-quality PDFs in Go with LightningPDF API in under 10 lines of code. Covers HTML to PDF, templates, batch generation, and best practices.

How to Generate PDFs in Go with LightningPDF

Generating PDFs in Go is a common requirement for invoices, reports, receipts, and other documents. While there are several Go PDF libraries available, using an API like LightningPDF offers better performance, easier maintenance, and more features than building your own solution.

PDF generation in Go is the process of programmatically creating PDF documents from HTML, templates, or structured data using Go's standard library or a cloud API, enabling automated document workflows in Go applications.

This tutorial covers everything you need to generate production-quality PDFs in Go using LightningPDF.

Why Use an API Instead of a Library?

Before we dive in, let's address the elephant in the room: why use an API instead of a Go library like gofpdf or gopdf?

Go PDF Libraries:

  • ❌ Limited HTML/CSS support
  • ❌ Manual layout calculations
  • ❌ No built-in templates
  • ❌ Memory-intensive for complex documents
  • ❌ Difficult to maintain (page breaks, fonts, etc.)

LightningPDF API:

  • ✅ Full HTML/CSS support (like Chrome)
  • ✅ Automatic layout calculations
  • ✅ 15+ starter templates
  • ✅ Sub-100ms generation for invoices
  • ✅ Zero maintenance (we handle infrastructure)

Let's get started.

Prerequisites

# Install Go (1.20+)
go version

# Create new project
mkdir pdf-demo && cd pdf-demo
go mod init pdf-demo

Quick Start: Your First PDF

Step 1: Get API Key

  1. Sign up at lightningpdf.dev/signup (free, 50 PDFs/month)
  2. Copy your API key from the dashboard

Step 2: Basic HTML to PDF

// main.go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

const (
    apiURL = "https://lightningpdf.dev/api/v1/pdf/generate"
    apiKey = "your_api_key_here" // Replace with your actual key
)

type PDFRequest struct {
    HTML   string `json:"html"`
    Format string `json:"format,omitempty"`
}

func generatePDF(html string) ([]byte, error) {
    // Prepare request
    reqBody := PDFRequest{
        HTML:   html,
        Format: "A4",
    }

    jsonData, err := json.Marshal(reqBody)
    if err != nil {
        return nil, fmt.Errorf("marshal error: %w", err)
    }

    // Make request
    req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
    if err != nil {
        return nil, fmt.Errorf("request error: %w", err)
    }

    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("http error: %w", err)
    }
    defer resp.Body.Close()

    // Check response
    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(resp.Body)
        return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
    }

    // Read PDF bytes
    pdfData, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("read error: %w", err)
    }

    return pdfData, nil
}

func main() {
    html := `
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                body { font-family: Arial, sans-serif; padding: 40px; }
                h1 { color: #333; }
            </style>
        </head>
        <body>
            <h1>Hello from Go!</h1>
            <p>This PDF was generated using LightningPDF API.</p>
        </body>
        </html>
    `

    pdf, err := generatePDF(html)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        os.Exit(1)
    }

    // Save to file
    err = os.WriteFile("output.pdf", pdf, 0644)
    if err != nil {
        fmt.Printf("Save error: %v\n", err)
        os.Exit(1)
    }

    fmt.Println("PDF generated successfully: output.pdf")
}

Run it:

go run main.go
# PDF generated successfully: output.pdf

Congratulations! You just generated your first PDF with Go.

Real-World Example: Invoice Generation

Let's build a proper invoice generator:

// invoice.go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "html/template"
    "net/http"
    "os"
    "time"
)

const apiKey = "your_api_key_here"

type Invoice struct {
    Number      string
    Date        time.Time
    DueDate     time.Time
    CustomerName string
    CustomerEmail string
    Items       []LineItem
    Subtotal    float64
    Tax         float64
    Total       float64
}

type LineItem struct {
    Description string
    Quantity    int
    Price       float64
    Total       float64
}

func (i *Invoice) Calculate() {
    i.Subtotal = 0
    for _, item := range i.Items {
        item.Total = float64(item.Quantity) * item.Price
        i.Subtotal += item.Total
    }
    i.Tax = i.Subtotal * 0.10 // 10% tax
    i.Total = i.Subtotal + i.Tax
}

const invoiceTemplate = `
<!DOCTYPE html>
<html>
<head>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Helvetica', 'Arial', sans-serif;
            padding: 40px;
            color: #333;
        }
        .header {
            display: flex;
            justify-content: space-between;
            margin-bottom: 40px;
            padding-bottom: 20px;
            border-bottom: 2px solid #4F46E5;
        }
        .company { font-size: 24px; font-weight: bold; color: #4F46E5; }
        .invoice-info { text-align: right; }
        .invoice-number { font-size: 18px; font-weight: bold; }
        .customer {
            margin-bottom: 30px;
            padding: 20px;
            background: #F3F4F6;
            border-radius: 8px;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin: 30px 0;
        }
        th {
            background: #4F46E5;
            color: white;
            padding: 12px;
            text-align: left;
        }
        td {
            padding: 12px;
            border-bottom: 1px solid #E5E7EB;
        }
        .text-right { text-align: right; }
        .totals {
            margin-left: auto;
            width: 300px;
            margin-top: 20px;
        }
        .totals tr td { border: none; padding: 8px; }
        .totals .total {
            font-size: 18px;
            font-weight: bold;
            padding-top: 12px;
            border-top: 2px solid #4F46E5;
        }
        .footer {
            margin-top: 60px;
            text-align: center;
            color: #6B7280;
            font-size: 12px;
        }
    </style>
</head>
<body>
    <div class="header">
        <div class="company">ACME Corp</div>
        <div class="invoice-info">
            <div class="invoice-number">Invoice #{{.Number}}</div>
            <div>Date: {{.Date.Format "Jan 2, 2006"}}</div>
            <div>Due: {{.DueDate.Format "Jan 2, 2006"}}</div>
        </div>
    </div>

    <div class="customer">
        <strong>Bill To:</strong><br>
        {{.CustomerName}}<br>
        {{.CustomerEmail}}
    </div>

    <table>
        <thead>
            <tr>
                <th>Description</th>
                <th class="text-right">Qty</th>
                <th class="text-right">Price</th>
                <th class="text-right">Total</th>
            </tr>
        </thead>
        <tbody>
            {{range .Items}}
            <tr>
                <td>{{.Description}}</td>
                <td class="text-right">{{.Quantity}}</td>
                <td class="text-right">${{printf "%.2f" .Price}}</td>
                <td class="text-right">${{printf "%.2f" .Total}}</td>
            </tr>
            {{end}}
        </tbody>
    </table>

    <table class="totals">
        <tr>
            <td>Subtotal:</td>
            <td class="text-right">${{printf "%.2f" .Subtotal}}</td>
        </tr>
        <tr>
            <td>Tax (10%):</td>
            <td class="text-right">${{printf "%.2f" .Tax}}</td>
        </tr>
        <tr class="total">
            <td>Total:</td>
            <td class="text-right">${{printf "%.2f" .Total}}</td>
        </tr>
    </table>

    <div class="footer">
        Thank you for your business!<br>
        Questions? Contact us at billing@acmecorp.com
    </div>
</body>
</html>
`

func generateInvoicePDF(invoice *Invoice) ([]byte, error) {
    // Calculate totals
    invoice.Calculate()

    // Render HTML template
    tmpl, err := template.New("invoice").Parse(invoiceTemplate)
    if err != nil {
        return nil, fmt.Errorf("template error: %w", err)
    }

    var htmlBuf bytes.Buffer
    if err := tmpl.Execute(&htmlBuf, invoice); err != nil {
        return nil, fmt.Errorf("template execute error: %w", err)
    }

    // Call LightningPDF API
    reqBody := map[string]interface{}{
        "html": htmlBuf.String(),
        "format": "A4",
    }

    jsonData, _ := json.Marshal(reqBody)
    req, err := http.NewRequest(
        "POST",
        "https://lightningpdf.dev/api/v1/pdf/generate",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        return nil, err
    }

    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("API error: %d", resp.StatusCode)
    }

    return io.ReadAll(resp.Body)
}

func main() {
    invoice := &Invoice{
        Number:        "INV-2026-001",
        Date:          time.Now(),
        DueDate:       time.Now().AddDate(0, 0, 30),
        CustomerName:  "Jane Smith",
        CustomerEmail: "jane@example.com",
        Items: []LineItem{
            {Description: "Website Development", Quantity: 1, Price: 5000.00},
            {Description: "Logo Design", Quantity: 2, Price: 500.00},
            {Description: "Hosting (Annual)", Quantity: 1, Price: 299.00},
        },
    }

    pdf, err := generateInvoicePDF(invoice)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        os.Exit(1)
    }

    filename := fmt.Sprintf("invoice-%s.pdf", invoice.Number)
    os.WriteFile(filename, pdf, 0644)
    fmt.Printf("Invoice generated: %s\n", filename)
}

This generates a professional invoice in <100ms using LightningPDF's native engine.

Using Templates

Instead of HTML strings, use LightningPDF templates:

type TemplateRequest struct {
    TemplateID string                 `json:"template_id"`
    Data       map[string]interface{} `json:"data"`
}

func generateFromTemplate(templateID string, data map[string]interface{}) ([]byte, error) {
    reqBody := TemplateRequest{
        TemplateID: templateID,
        Data:       data,
    }

    jsonData, _ := json.Marshal(reqBody)
    req, _ := http.NewRequest(
        "POST",
        "https://lightningpdf.dev/api/v1/pdf/generate",
        bytes.NewBuffer(jsonData),
    )

    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

// Usage
pdf, err := generateFromTemplate("invoice-v2", map[string]interface{}{
    "invoice_number": "INV-001",
    "customer_name":  "John Doe",
    "amount":         1000.00,
})

Batch Generation

Generate multiple PDFs in one API call:

type BatchRequest struct {
    TemplateID string                   `json:"template_id"`
    Data       []map[string]interface{} `json:"data"`
}

func generateBatch(templateID string, invoices []Invoice) ([]byte, error) {
    // Convert invoices to data array
    data := make([]map[string]interface{}, len(invoices))
    for i, inv := range invoices {
        data[i] = map[string]interface{}{
            "invoice_number": inv.Number,
            "customer_name":  inv.CustomerName,
            "total":          inv.Total,
            // ... more fields
        }
    }

    reqBody := BatchRequest{
        TemplateID: templateID,
        Data:       data,
    }

    jsonData, _ := json.Marshal(reqBody)
    req, _ := http.NewRequest(
        "POST",
        "https://lightningpdf.dev/api/v1/pdf/batch",
        bytes.NewBuffer(jsonData),
    )

    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    // Returns ZIP file with all PDFs
    return io.ReadAll(resp.Body)
}

Error Handling

Production-ready error handling:

type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func generatePDFWithRetry(html string, maxRetries int) ([]byte, error) {
    var lastErr error

    for attempt := 0; attempt < maxRetries; attempt++ {
        pdf, err := generatePDF(html)
        if err == nil {
            return pdf, nil
        }

        lastErr = err

        // Exponential backoff
        backoff := time.Duration(attempt+1) * time.Second
        time.Sleep(backoff)
    }

    return nil, fmt.Errorf("failed after %d attempts: %w", maxRetries, lastErr)
}

Best Practices

1. Reuse HTTP Client

var httpClient = &http.Client{
    Timeout: 30 * time.Second,
}

// Use httpClient instead of http.DefaultClient

2. Use Context for Timeouts

func generatePDFWithContext(ctx context.Context, html string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(
        ctx,
        "POST",
        apiURL,
        bytes.NewBuffer(jsonData),
    )
    // ... rest of request
}

3. Pool Goroutines for Batch Jobs

func generateManyPDFs(invoices []Invoice) error {
    sem := make(chan struct{}, 10) // Max 10 concurrent
    errCh := make(chan error, len(invoices))

    for _, inv := range invoices {
        go func(invoice Invoice) {
            sem <- struct{}{}        // Acquire
            defer func() { <-sem }() // Release

            pdf, err := generateInvoicePDF(&invoice)
            if err != nil {
                errCh <- err
                return
            }

            // Save PDF...
        }(inv)
    }

    // Wait and check errors...
    return nil
}

Conclusion

LightningPDF makes PDF generation in Go simple and fast:

  • Sub-100ms for invoices (native engine)
  • No dependencies (just net/http)
  • Production-ready templates
  • Batch API for high volumes
  • Zero maintenance (we handle infrastructure)

Ready to start? Get your free API key — 50 PDFs/month, no credit card required.

Next steps:

Frequently Asked Questions

How do I generate a PDF in Go?

The simplest way to generate PDFs in Go is using the LightningPDF API with Go's standard net/http package. Send a POST request with your HTML content to the API endpoint, and receive PDF bytes in the response. No external dependencies or browser installations are required.

Is LightningPDF faster than gofpdf for PDF generation?

LightningPDF's native engine generates PDFs in under 100 milliseconds, comparable to or faster than local Go libraries like gofpdf. The key advantage is full HTML and CSS support without manual layout calculations, plus template management and batch processing that local libraries cannot provide.

Can I generate PDFs in Go without a browser?

Yes, LightningPDF's native Go engine generates PDFs without launching a browser, achieving sub-100ms performance for invoices, receipts, and reports. For complex layouts requiring full CSS support, the API automatically routes to a Chromium engine, but your Go code stays the same.

How do I batch generate PDFs in Go?

LightningPDF's batch API lets you generate hundreds of PDFs in a single HTTP request from Go. Send an array of data objects with a template ID, and receive a ZIP file containing all generated PDFs. You can also use Go goroutines with a semaphore pattern for concurrent individual requests.

L

LightningPDF Team

Building fast, reliable PDF generation tools for developers.

Ready to generate PDFs?

Start free with 50 PDFs per month. No credit card required.

Get Started Free