Golang: How To Guide
How To Create a PDF in Go: a Step-by-Step Tutorial
Follow Along As We Generate a PDF Invoice From Scratch

Introduction
If you haven’t lived under a rock for the last 20 years, you’ll know that PDFs can be difficult to work with, especially when dealing with dynamic content (e.g., personalized information). In the case of invoices, this becomes increasingly prominent and clear.
I mean, nobody wants to manually change information on each PDF. Do they?

It would be much easier if it were stored in a database somehow and a script was created that extracted the necessary data to pass it to a library. That way you could scale it (and save yourself a few headaches in the process).
And that’s exactly what we’ll do here.
It’s worth noting that in some cases, a user can generate a PDF invoice on the front end, while in others, a system will generate one in the background and email it to the user at the end of a billing cycle. We’ll specifically address the latter.
📌 Disclaimer: I chose UniDoc to help me with this because it has a wide range of customization, extensive documentation, examples, and is quite honestly, simple to set up and use. But there are a couple other PDF Go packages that you can also use, such as Gopdf, Gofpdf (deprecated), and Pdfcpu. PDFTron also has a Go SDK, but it’s not in native Go so it requires some dependencies.
Just note that whatever your preference, the logic remains the same; a package is simply a means to an end.
What you will learn
- Why you should sketch a PDF invoice before writing code to generate it
- How to visualize a simple architectural design for it
- How to create a
invoice-pdf.go
file - How to create a PDF invoice: general structure, header, letterhead, invoice items, and footer
- How to generate the final output of your invoice PDF
- [Bonus] Why you shouldn’t use float with money and currency
Prerequisites
- Knowledge of Go and Go modules
- Go 1.18+ installed on your machine

Getting started
You can follow along here or have a look at the code base on this GitHub repository:
First, navigate to the directory where you can create your Go applications and create a new directory called invoice-pdf-generator.
Now navigate to the project root directory and create a Go module by running the instructions below in the terminal within the project root directory:
go mod init github.com/username/invoice-pdf-generator
👉 Note: Please substitute your Github username for [username] above
📖 Further Reading: We’ll be using Go modules to manage our project dependencies; you can learn more about them here.

PDF sketch
Now that we’ve set up our project and are ready to code, let’s create a simple visual draft of what our invoice PDF should look like:

💡 Before starting on any project, I’ve personally learned it best to envision what the output should be. This way you can stay focused on what needs-to-be-done, and linearly structure your problem-solving thought process.
From the image above you can see the design blocks of data for our invoice PDF.
Simple architecture design
Let’s understand the data flow a bit more, and visualize the architectural design:

Here, our invoice pdf generator service is a simple program that simulates accepting invoice data requests with the data provided to generate an invoice PDF, which is then stored in an s3 bucket or preferred storage medium.
Open the project in your favorite Go IDE and create amain.go
file. Add the package declaration as main and the main function:
package main
func main() {
}
👉 Note: We will come back to the
main.go
file and add code later
Next, inside the project root directory, create a new directory called internal
. Inside the internal
directory, create invoice-data.go
and copy the content below into invoice-data.go
📖 Further Reading: Read more about using
internal
directory in Go here)
package internal
import "errors"
type InvoiceData struct {
Title string
Quantity int64
Price int64
TotalAmount int64
}
func (d *InvoiceData) CalculateTotalAmount() int64 {
totalAmount := d.Quantity * d.Price
return totalAmount
}
func (d *InvoiceData) ReturnItemTotalAmount() float64 {
totalAmount := d.CalculateTotalAmount()
converted := float64(totalAmount) / 100
return converted
}
func (d *InvoiceData) ReturnItemPrice() float64 {
converted := float64(d.Price) / 100
return converted
}
func NewInvoiceData(title string, qty int64, price interface{}) (*InvoiceData, error) {
var convertedPrice int64
switch priceValue := price.(type) {
case int64:
convertedPrice = priceValue * 100
case int:
convertedPrice = int64(priceValue * 100)
case float32:
convertedPrice = int64(priceValue * 100)
case float64:
convertedPrice = int64(priceValue * 100)
default:
return nil, errors.New("type not permitted")
}
return &InvoiceData{
Title: title,
Quantity: qty,
Price: convertedPrice,
}, nil
}
The NewInvoiceData
function is a simple constructor for our InvoiceData
, a struct that is used to create new InvoiceData
data.
The NewInvoiceData
allows interface
(any data type) and the switch
statement filters the allowed data type. This is deliberately added.
👉 Note: I will refactor this code base with Go generics in a future article "Go Generics In-Depth.”
You may have noticed that on lines 33, 35, 37, and 39, I multiplied the priceValue
by 100 and divided it by 100 in lines 19 and 25. As a result, multiplyingpriceValue
by 100 (or the Euro cent approach) reduces missed calculations in our invoice service.
💡 Expert advice: When working with money or currency values, it is not recommended to use float. Computers treat floating numbers as base 2 rather than base 10 (which we use in money calculations). More information can be found here.

👉 Note:
CalculateTotalAmount
calculates the total amount of a single invoice item by multiplying its quantity and price.
Within the same directory, please copy the following content into the invoice.go
file:
package internal
type Invoice struct {
Name string
Address string
InvoiceItems []*InvoiceData
}
func CreateInvoice(name string, address string, invoiceItems []*InvoiceData) *Invoice {
return &Invoice{
Name: name,
Address: address,
InvoiceItems: invoiceItems,
}
}
func (i *Invoice) CalculateInvoiceTotalAmount() float64 {
var invoiceTotalAmount int64 = 0
for _, data := range i.InvoiceItems {
amount := data.CalculateTotalAmount()
invoiceTotalAmount += amount
}
totalAmount := float64(invoiceTotalAmount) / 100
return totalAmount
}
Above, you’ll see we added theCreateInvoice
function to theinvoice.go
file, which accepts a businessname
, address
, and a slice ofinvoiceItems
to create anInvoice
instance.
CalculateInvoiceTotalAmount
computed the total invoice amount by looping through theinvoiceItems
slice (array) and obtaining eachInvoiceData
TotalAmount
via theCalculateTotalAmount
method.
On line 24, we converted the integer to a float and then divided it by 100 (see here for an explanation).
Now that that’s done, let’s get the API key we need to complete this invoice PDF.

Creating the invoice-pdf.go
file
Please head to UniDoc and access a free API key by clicking here.
Don’t forget to copy the API key after creating it.

🎥 Video: For a short tutorial, I found this on the web which may be helpful
Back to our project. In our project root directory, create an environment file .env
and add the content below:
👉 Note: If you git clone the project from GitHub, copy the .env.example to .env file.
UNIDOC_LICENSE_API_KEY=<Your_API_Key>
🛑 DO NOT commit .env file with the API key.
Replace [Your_API_Key] with your account key from your UniDoc account.
Within the project root directory in the terminal, run this command to add thegodotenv
package, which will help read our environment variables:
go get -u github.com/joho/godotenv
Next, let’s create a config directory in the project root directory and create aconfig.go
file inside it. Then copy the content below to it:
package config
import (
"errors"
"github.com/joho/godotenv"
"os"
)
type UniDocConfig struct {
Key string
}
func GetUniDocCred() (*UniDocConfig, error) {
err := godotenv.Load()
if err != nil {
return nil, err
}
key := os.Getenv("UNIDOC_LICENSE_API_KEY")
if len(key) == 0 {
return nil, errors.New("uni doc key not found")
}
return &UniDocConfig{Key: key}, nil
}
On line 14, we loaded the.env
file to get environment variables from the.env
file which isUNIDOC_LICENSE_API_KEY
in this project. The rest of the code is just checking if the key is set and returning the UniDocConfig struct or an error.
We are almost there. Let’s create theinvoice-pdf.go
file insideinternal
directory and add our PDF-generating code to it, which is the soul of our simple invoice PDF generator project.
But before that, navigate back to the project root directory in the terminal and add the UniDoc Go package as shown below:
go get -u github.com/unidoc/unipdf/v3
👉 Note: This article is based on version 3 of the package
Now, navigate to the project's internal directory, and within the internal directory, create invoice-pdf.go
and add the code below:
package internal
import (
"fmt"
"github.com/unidoc/unipdf/v3/common/license"
"github.com/unidoc/unipdf/v3/creator"
"marvinhosea/invoices/config"
"strings"
)
type Client struct {
creator *creator.Creator
}
type cellStyle struct {
ColSpan int
HAlignment creator.CellHorizontalAlignment
BackgroundColor creator.Color
BorderSide creator.CellBorderSide
BorderStyle creator.CellBorderStyle
BorderWidth float64
BorderColor creator.Color
Indent float64
}
var cellStyles = map[string]cellStyle{
"heading-left": {
BackgroundColor: creator.ColorRGBFromHex("#332f3f"),
HAlignment: creator.CellHorizontalAlignmentLeft,
BorderColor: creator.ColorWhite,
BorderSide: creator.CellBorderSideAll,
BorderStyle: creator.CellBorderStyleSingle,
BorderWidth: 6,
},
"heading-centered": {
BackgroundColor: creator.ColorRGBFromHex("#332f3f"),
HAlignment: creator.CellHorizontalAlignmentCenter,
BorderColor: creator.ColorWhite,
BorderSide: creator.CellBorderSideAll,
BorderStyle: creator.CellBorderStyleSingle,
BorderWidth: 6,
},
"left-highlighted": {
BackgroundColor: creator.ColorRGBFromHex("#dde4e5"),
HAlignment: creator.CellHorizontalAlignmentLeft,
BorderColor: creator.ColorWhite,
BorderSide: creator.CellBorderSideAll,
BorderStyle: creator.CellBorderStyleSingle,
BorderWidth: 6,
},
"centered-highlighted": {
BackgroundColor: creator.ColorRGBFromHex("#dde4e5"),
HAlignment: creator.CellHorizontalAlignmentCenter,
BorderColor: creator.ColorWhite,
BorderSide: creator.CellBorderSideAll,
BorderStyle: creator.CellBorderStyleSingle,
BorderWidth: 6,
},
"left": {
HAlignment: creator.CellHorizontalAlignmentLeft,
},
"centered": {
HAlignment: creator.CellHorizontalAlignmentCenter,
},
"gradingsys-head": {
HAlignment: creator.CellHorizontalAlignmentLeft,
},
"gradingsys-row": {
HAlignment: creator.CellHorizontalAlignmentCenter,
},
"conduct-head": {
HAlignment: creator.CellHorizontalAlignmentLeft,
},
"conduct-key": {
HAlignment: creator.CellHorizontalAlignmentLeft,
},
"conduct-val": {
BackgroundColor: creator.ColorRGBFromHex("#dde4e5"),
HAlignment: creator.CellHorizontalAlignmentCenter,
BorderColor: creator.ColorWhite,
BorderSide: creator.CellBorderSideAll,
BorderStyle: creator.CellBorderStyleSingle,
BorderWidth: 3,
},
}
func GenerateInvoicePdf(invoice Invoice) error {
conf, err := config.GetUniDocCred()
if err != nil {
return err
}
err = license.SetMeteredKey(conf.Key)
if err != nil {
return err
}
c := creator.New()
c.SetPageMargins(40, 40, 0, 0)
cr := &Client{creator: c}
err = cr.generatePdf(invoice)
if err != nil {
return err
}
return nil
}
func (c *Client) generatePdf(invoice Invoice) error {
rect := c.creator.NewRectangle(0, 0, creator.PageSizeLetter[0], 120)
rect.SetFillColor(creator.ColorRGBFromHex("#dde4e5"))
rect.SetBorderWidth(0)
err := c.creator.Draw(rect)
if err != nil {
return err
}
headerStyle := c.creator.NewTextStyle()
headerStyle.FontSize = 50
table := c.creator.NewTable(1)
table.SetMargins(0, 0, 20, 0)
err = drawCell(table, c.newPara("Sample Invoice", headerStyle), cellStyles["centered"])
if err != nil {
return err
}
err = c.creator.Draw(table)
if err != nil {
return err
}
err = c.writeInvoice(invoice)
if err != nil {
return err
}
err = c.creator.WriteToFile(strings.ToLower(invoice.Name) + "_invoice.pdf")
if err != nil {
return err
}
return nil
}
func (c *Client) newPara(text string, textStyle creator.TextStyle) *creator.StyledParagraph {
p := c.creator.NewStyledParagraph()
p.Append(text).Style = textStyle
p.SetEnableWrap(false)
return p
}
func drawCell(table *creator.Table, content creator.VectorDrawable, cellStyle cellStyle) error {
var cell *creator.TableCell
if cellStyle.ColSpan > 1 {
cell = table.MultiColCell(cellStyle.ColSpan)
} else {
cell = table.NewCell()
}
err := cell.SetContent(content)
if err != nil {
return err
}
cell.SetHorizontalAlignment(cellStyle.HAlignment)
if cellStyle.BackgroundColor != nil {
cell.SetBackgroundColor(cellStyle.BackgroundColor)
}
cell.SetBorder(cellStyle.BorderSide, cellStyle.BorderStyle, cellStyle.BorderWidth)
if cellStyle.BorderColor != nil {
cell.SetBorderColor(cellStyle.BorderColor)
}
if cellStyle.Indent > 0 {
cell.SetIndent(cellStyle.Indent)
}
return nil
}
func (c *Client) writeInvoice(invoice Invoice) error {
headerStyle := c.creator.NewTextStyle()
// Invoice Header info table.
table := c.creator.NewTable(2)
table.SetMargins(0, 0, 50, 0)
err := drawCell(table, c.newPara("Business: "+invoice.Name, headerStyle), cellStyles["left"])
if err != nil {
return err
}
err = drawCell(table, c.newPara("Address: "+invoice.Address, headerStyle), cellStyles["left"])
if err != nil {
return err
}
err = c.creator.Draw(table)
if err != nil {
return err
}
// Invoice items table.
table = c.creator.NewTable(4)
table.SetMargins(0, 0, 20, 0)
err = table.SetColumnWidths(0.4, 0.2, 0.2, 0.2)
if err != nil {
return err
}
headingStyle := c.creator.NewTextStyle()
headingStyle.FontSize = 20
headingStyle.Color = creator.ColorRGBFromHex("#fdfdfd")
regularStyle := c.creator.NewTextStyle()
// Draw table header.
err = drawCell(table, c.newPara(" Title", headingStyle), cellStyles["heading-left"])
if err != nil {
return err
}
err = drawCell(table, c.newPara("Quantity", headingStyle), cellStyles["heading-centered"])
if err != nil {
return err
}
err = drawCell(table, c.newPara("Price", headingStyle), cellStyles["heading-centered"])
if err != nil {
return err
}
err = drawCell(table, c.newPara("Total", headingStyle), cellStyles["heading-centered"])
if err != nil {
return err
}
for _, datum := range invoice.InvoiceItems {
err = drawCell(table, c.newPara(" "+datum.Title, regularStyle), cellStyles["left-highlighted"])
if err != nil {
return err
}
err = drawCell(table, c.newPara(fmt.Sprintf("%v", datum.Quantity), regularStyle), cellStyles["centered-highlighted"])
if err != nil {
return err
}
err = drawCell(table, c.newPara(fmt.Sprintf("%v", datum.Price), regularStyle), cellStyles["centered-highlighted"])
if err != nil {
return err
}
err = drawCell(table, c.newPara(fmt.Sprintf("%v", datum.CalculateTotalAmount()), regularStyle), cellStyles["centered-highlighted"])
if err != nil {
return err
}
}
err = c.creator.Draw(table)
if err != nil {
return err
}
boldStyle := c.creator.NewTextStyle()
boldStyle.FontSize = 16
grid := c.creator.NewTable(12)
grid.SetMargins(0, 0, 50, 0)
gradeInfoStyle := c.creator.NewTextStyle()
table = c.creator.NewTable(2)
err = table.SetColumnWidths(0.6, 0.4)
if err != nil {
return err
}
err = drawCell(table, c.newPara("Total Amount:", gradeInfoStyle), cellStyles["conduct-key"])
if err != nil {
return err
}
err = drawCell(table, c.newPara(fmt.Sprintf("%v", invoice.CalculateInvoiceTotalAmount()), gradeInfoStyle), cellStyles["conduct-val"])
if err != nil {
return err
}
err = grid.MultiColCell(5).SetContent(table)
if err != nil {
return err
}
err = c.creator.Draw(grid)
if err != nil {
return err
}
return nil
}
General structure: a quick analysis ☝️
On line 11, in theinvoice-pdf.go
file, we added a Client
struct with a property creator which holds our PDF creator instance. The creator instance has methods that help in manipulating the PDF document and creating a nice invoice in PDF format.
On line 26, we defined the cell styles to be added to the document.
On line 88, using theconfig
package with theGetUniDocCred
function, we retrieved our API key from the environment variable that we added in the.env
file. We’ll see an error returned if we fail to get the document.
On line 93, since our UniDoc usage is metered, we were required to add our account API key retrieved on line 88.
On line 98, we initialized and created a newcreator
instance.
On line 99, we set the PDF page margins.
On line 101, we called theClient
structgeneratePdf
method and passed the invoice instance to it.
Let’s investigate a bit further into this method.
Creating the invoice header
Lines 110 to 116 began the creation of our invoice header, a 120-height rectangular block with a light gray background. You can experiment with the argument based on your preferences.
On line 118, we created a new text style and assign it a font size of 50 on line 119.
After initializing the style, we created a table with one column and a margin of zero on both the left, right, and bottom, and a top margin of 20 on lines 121 and 122.
On line 123, we created a new centered cell block with a new paragraph of text style created in line 118 and put it in the current position on our table. The new paragraph contains the header content.
On line 127, we made our first table block.
Creating the invoice business letterhead
Now that we have added the invoice header, let’s add the business information.
From line 132, we jumped to line 176 using thewriteInvoice
method that accepts an invoice instance.
On line 177, all we did was create another text style for the table business letterhead.
From lines 179 to 192, we created another table with two columns (instead of one) and a top margin of 50. At this point, you should be familiar with the table block creation code.
Adding invoice items
From 195 to 204, we initialized the table that holds our invoice item data. The process is the same as before, but with some customization like column widths, font size, etc.
Then, from lines 207 to 222, we drew the invoice items' table header.
👉 Note: This should be very familiar if you know HTML and CSS
On line 223, we looped through each and every invoice item and added the respective data into the invoice item table cells. Then we positioned them accordingly inside the table at their current positions.
We finalized the invoice item table creation on line 241, which then made the table block.

[sorry, I had to 😂]
Creating the invoice footer
It is common knowledge that an invoice must have a summary of its items and a total sum. So let’s finish strong by adding this in the document footer:
Starting with line 246, we created another table with twelve columns and set the column widths in line 253.
On lines 257 and 261, we provided the footer table cell data.
👉 Note: if your code was successful, it would have returned a nil error back on line 132
Line 137 is where the magic happens. We wrote thecreator
blocks into the output document, which is a PDF in this case, and the method creates the PDF at the specified path.
☝️Remember: Do not forget to add
Finally, let’s go back to ourmain.go
file and add the code below.
package main
import (
"fmt"
internal "marvinhosea/invoices/internal"
)
func main() {
// Generate sample invoice data
ecommerceInvoiceData, err := internal.NewInvoiceData("Ecommerce application", 1, 3000.50)
if err != nil {
panic(err)
}
laptopInvoiceData, err := internal.NewInvoiceData("Macbook Pro", 1, 200.70)
if err != nil {
panic(err)
}
// Invoice Items collection
invoiceItems := []*internal.InvoiceData{ecommerceInvoiceData, laptopInvoiceData}
// Create single invoice
invoice := internal.CreateInvoice("Example Shop1", "Example address", invoiceItems)
err = internal.GenerateInvoicePdf(*invoice)
fmt.Printf("The Total Invoice Amount is: %f", invoice.CalculateInvoiceTotalAmount())
}
On lines 10 and 14, we simply gathered new invoice data objects. [Assume that this is the invoice data request]
And on line 19, we created a slice of InvoiceData
as an invoice items collection.
The created invoice items collection was used to create an invoice instance on line 22, which was used to generate an invoice PDF file on line 23.

Finishing up
Now go ahead and run the program in the terminal. If there is no error, you should get the following output:
The Total Invoice Amount is: 3201.200000
Now, go ahead and check the project root directory. You should find a pdf invoice file that looks like this:

And that’s it for a basic PDF invoice!
Questions/Troubles/Concerns? Post them below in the Comments👇 and I’ll make sure to answer them as quickly as possible.

Conclusion
Generating PDFs, especially those with dynamic content, is not easy. But luckily, there are a few options out there to help you — from the design to architecture to system implementation.
Obviously, I did not go into much detail with this article, but hopefully I set the building blocks in place for you to customize PDF invoices yourself using Go.
Takeaways:
- Create a visual document (pdf) draft before implementing
- Construct a system architecture diagram gives a vision of the project
- Do NOT use floating data type for money and currency calculation
- Do NOT commit or track project environment files with keys with git
🚀 Bonus Challenge: Add more implementation to the code base, such as storing PDF invoices files in an S3 bucket.

Source code
You can find the project’s source code at the GitHub link below:
Other Articles To Consider
How To Create a Go Private Module With Docker A Step-By-Step Frameworkmedium.com
Streamline Your Productivity With “Air” Go’s Best Method For Live Reload Development With Dockermedium.com

Disclosure: In accordance with Medium.com’s rules and guidelines, I publicly acknowledge financial compensation from UniDoc for this article. All thoughts, opinions, code, pictures, writing, etc. are my own.