Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 27

Formatted Terminal Output with `fmt` and `text/tabwriter`

Recommended resources

Hello! Welcome to the next lesson in our journey to build powerful DevOps tools with Go.

In the last lesson, we focused on how to make our tools aware of their environment. You learned to read configuration from environment variables using the os package and to handle file paths in a cross-platform way with path/filepath. These skills are about getting data into your application.

Today, we'll tackle the other side of the coin: getting data out in a clean, professional, and readable way. The learning outcome for this lesson is to produce formatted and column-aligned terminal output using fmt and text/tabwriter.

Think about the CLI tools you use every day, like kubectl get pods, docker ps, or git status. Their output is structured, aligned, and easy to parse at a glance. This lesson will give you the skills to build that same level of professionalism into your own Go applications.

1. The fmt Package: Your Formatting Workhorse

The fmt package is one of the first packages you encounter in Go, and for good reason. It's the standard library's solution for formatted I/O. While you've likely used fmt.Println already, the package offers much more control.

The functions in fmt generally come in three families:

  • Print...: Writes to standard output (os.Stdout).
  • Fprint...: Writes to a specified io.Writer. This is incredibly useful for directing output to files, network connections, or, as we'll see, standard error (os.Stderr).
  • Sprint...: Formats data into a string variable in memory, without printing it.
GoLand IDE showing Cobra CLI setup and error handling with fmt.Fprintf
This image from the GoLand IDE guide shows a practical use of `fmt.Fprintf`. The program directs a formatted error message specifically to `os.Stderr`, separating error output from standard output—a best practice in CLI design.

Fine-Grained Control with fmt.Printf

fmt.Printf is the key to precise formatting. It uses "format verbs" (placeholders like %s or %d) to control how data is represented in the output string.

The following video provides an excellent introduction to the fmt package, covering the differences between Println and Printf, and introducing the most common format verbs.

Go's full output power: fmt, ANSI Colors & (s)logging

Watch this segment from the video "Go's full output power" by Flo Woelki to understand the fundamentals of formatted printing.

Watch from the beginning to 05:48. Pay attention to: The key differences between fmt.Println and fmt.Printf (00:09 - 02:55). The table of format verbs like %s (string), %d (decimal), %f (float), %v (default value), and %+v (struct with field names) (02:55 - 05:48). After that, jump to 09:46 - 11:00 to see how fmt.Sprintf is used to format a string into a variable for later use.

As you saw, verbs like %v, %T, and %+v are fantastic for debugging, while %s, %d, and %f give you control over production output.

Manual Alignment: Width and Precision

You can add numbers to format verbs to control width and padding. This is a first step towards creating aligned columns, though as we'll see, it has its limits.

  • %10s: Pads a string with spaces on the left to be 10 characters wide.
  • %-10s: Pads a string with spaces on the right (left-justifies it) to be 10 characters wide.
  • %.2f: Formats a floating-point number with 2 decimal places of precision.
  • %04d: Pads an integer with leading zeros to be 4 digits wide.

This next video provides a hands-on demonstration of these features.

Golang Tutorial #4 - Printing to Console & fmt

This clip from Tech With Tim's tutorial on the fmt package specifically covers how to use width and precision specifiers to control alignment and padding.

Watch from 09:04 to 12:36. This section clearly demonstrates: How to use a number (e.g., %9s) to specify width and pad with spaces. How to use a negative number (%-9s) for left-justification. How to control decimal precision (.2f). How to pad with leading zeros (%07d). Finally, the video briefly mentions \t (tab) and \n (newline) from 13:08 to 14:00, which are essential characters for structuring text output.

Let's try a quick example. Imagine printing a list of running services:

package main

import "fmt"

func main() {
    fmt.Printf("%-15s %-10s %5s\n", "SERVICE", "STATUS", "PORT")
    fmt.Printf("%-15s %-10s %5d\n", "api-gateway", "Running", 8080)
    fmt.Printf("%-15s %-10s %5d\n", "user-database", "Running", 5432)
    fmt.Printf("%-15s %-10s %5d\n", "auth-service", "Failed", 8081)
}

This produces a nicely aligned table:

SERVICE         STATUS     PORT
api-gateway     Running     8080
user-database   Running     5432
auth-service    Failed      8081

However, this manual approach is brittle. If a service name were longer than 15 characters, the alignment would break. For robust, dynamic tables, we need a better tool.


2. Professional Tables with text/tabwriter

Go's standard library provides the text/tabwriter package to solve the problem of column alignment once and for all. It works by acting as a filter: you write tab-delimited text to it, and it automatically calculates the required padding to make all columns align perfectly.

The core workflow is simple:

  1. Create a new tabwriter.Writer.
  2. Write your content to it, using tab characters (\t) to separate columns and newlines (\n) to separate rows. You typically use fmt.Fprintf for this.
  3. Flush the writer to produce the final, formatted output on its destination io.Writer.

The following article is a fantastic guide to tabwriter. We'll work through it step-by-step.

A Guide to Go's `text/tabwriter` Package: Aligning Text

First, let's understand the core concept and basic usage pattern of tabwriter. This article, "A Guide to Go's text/tabwriter Package," provides clear explanations and code examples.

Please read the first three sections: Understanding the text/tabwriter Package, Core Concepts and the Writer Structure, and Basic Usage Pattern. Focus on the roles of the character and the essential w.Flush() call.

Let's rewrite our previous service list example using tabwriter:

package main

import (
	"fmt"
	"os"
	"text/tabwriter"
)

func main() {
	// 1. Create a new tabwriter that writes to os.Stdout
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) // minwidth, tabwidth, padding, padchar, flags

	// 2. Write tab-separated values
	fmt.Fprintln(w, "SERVICE\tSTATUS\tPORT")
	fmt.Fprintln(w, "api-gateway\tRunning\t8080")
	fmt.Fprintln(w, "user-database-with-a-very-long-name\tRunning\t5432") // Alignment holds!
	fmt.Fprintln(w, "auth-service\tFailed\t8081")

	// 3. Flush the writer to print the formatted output
	w.Flush()
}

Output:

SERVICE                                  STATUS    PORT
api-gateway                              Running   8080
user-database-with-a-very-long-name      Running   5432
auth-service                             Failed    8081

Notice how tabwriter automatically handled the long service name, keeping all columns aligned.

Configuring tabwriter

You can customize the output by changing the parameters in tabwriter.NewWriter. The most important ones are minwidth, padding, and flags.

A Guide to Go's `text/tabwriter` Package: Aligning Text

Now let's explore how to configure the tabwriter's behavior. Please continue with the same article.

Read the sections Configuration Parameters Explained, Practical Configuration Examples, and Working with Formatting Flags. Pay special attention to the AlignRight flag for numbers and the Debug flag for troubleshooting. Then, review the Real-World Use Cases, which shows a process monitor example highly relevant to your DevOps work. Finally, and most importantly, read Common Pitfalls and Solutions. Forgetting to call Flush() is the most common mistake new users make!

A common and robust pattern is to use defer to ensure Flush() is always called.

func printTable() {
    w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
    defer w.Flush() // Guarantees the buffer is flushed before the function returns.

    fmt.Fprintln(w, "ID\tNAME\tSTATUS")
    fmt.Fprintln(w, "1\tnginx\tRunning")
    fmt.Fprintln(w, "2\tpostgres\tRunning")
}
Test your understanding!

Modify the service list example to right-align the PORT column. Hint: You'll need to use a flag and potentially separate the header from the data to apply different alignment, or simply accept that the "PORT" header will also be right-aligned.

Show answer

A simple approach is to apply tabwriter.AlignRight to the entire writer. This will right-align all columns.

package main

import (
	"fmt"
	"os"
	"text/tabwriter"
)

func main() {
	// Use the AlignRight flag
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.AlignRight)
	defer w.Flush()

	fmt.Fprintln(w, "SERVICE\tSTATUS\tPORT")
	fmt.Fprintln(w, "api-gateway\tRunning\t8080")
	fmt.Fprintln(w, "user-database-with-a-very-long-name\tRunning\t5432")
	fmt.Fprintln(w, "auth-service\tFailed\t8081")
}

Output:

                              SERVICE      STATUS   PORT
                          api-gateway     Running   8080
user-database-with-a-very-long-name     Running   5432
                         auth-service      Failed   8081

Notice how the PORT column is now neatly aligned on the right, which is standard for numeric data.


3. Putting It All Together: A Reusable Table Printer

For larger applications, you'll want to encapsulate the tabwriter logic into a reusable helper function. This separates your data-gathering logic from your presentation logic, leading to cleaner code.

The following resource presents a complete, practical example of creating a generic PrintTable function. This pattern is something you can directly adapt for your own projects.

Display tables in the CLI using just the Go standard library

This article, "Display tables in the CLI using just the Go standard library," builds a reusable function around tabwriter. This is a great example of turning a standard library feature into a powerful, project-specific utility.

Study the code in the printer/table_printer.go and main.go files. Pay close attention to: The PrintTable function signature: how it accepts an io.Writer, headers, and a function to get rows. The userToRows function in main.go: this shows the important step of transforming your application's data structures (like a user struct) into the [][]string format that the printer needs. The final output, which demonstrates how multi-line entries for a single user are handled gracefully.

This approach is powerful because the PrintTable function doesn't need to know anything about users or permissions. It only deals with strings, making it a truly generic and reusable component for any tabular data you need to display.


Conclusion

You've now added a crucial skill to your Go toolkit: creating professional terminal output. This ability to present data clearly is what separates a quick script from a polished, user-friendly tool.

Key Takeaways:

  • Use fmt.Printf with format verbs (%s, %d, %v) for fine-grained control over string formatting.
  • Use fmt.Sprintf when you need to format data into a string variable for later use.
  • text/tabwriter is the standard and robust way to create column-aligned tables.
  • The tabwriter workflow is: NewWriter, write tab-delimited content, and Flush.
  • Always remember to call w.Flush(). Using defer w.Flush() right after creating the writer is a failsafe pattern.
  • For complex applications, encapsulate tabwriter logic in a reusable helper function that separates data transformation from presentation.

Next Up

You now know how to parse arguments, interact with the OS, handle data formats like JSON/YAML, and produce clean, formatted output. You have all the pieces to build a complete and useful CLI tool. In our next lesson, we'll cover the final step in the development lifecycle: creating installable, cross-compiled binaries for different operating systems. This will allow you to easily distribute your new tools to colleagues and servers, regardless of their platform.