Back to Blog

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 ·

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.

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
  • ✅ 50+ pre-built 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/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/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/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/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:

Ready to generate PDFs?

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

Get Started Free