Golang Project Structure

Tips and tricks for writing and structuring Go code

Guide to Printing to the Console in Go

Language

  • unknown

by

Most programs will need to emit some kind of output if they’re going to be useful to the people using them.

So today we will look at the various ways to print content to the console in Go, focusing on four related functions in particular.

Introducing the Fmt Package

The Go standard library’s functions for printing to the console that we will look at are all contained within the "fmt" package.

A row of wooden letters spelling out the word "format".
Formatting output involves presenting it in a way that is attractive for humans. This is just as important as printing it.

The package name "fmt" is short for the word “format”, since the functions handle the presentation of output as well as actually emitting it to the console.

Using the Print Function

The simplest function that can be used to emit output to the console is fmt.Print, which takes an arbitrary number of arguments of any type and converts them to human-readable strings, so that they can be concatenated together and printed out to the console.

package main

import "fmt"

func main() {
	fmt.Print(99, "test", []string{"cat", "dog", "goldfish"})
}

Running the program above should print the following output:

99test[cat dog goldfish]

We can see how the arguments have been concatenated together without putting any whitespace between them.

Return Values of the Print Function

The fmt.Print function has two return values. The first is an integer, which holds the number of bytes written to the console, and the second is an error value.

However, these return values are rarely used, since it’s generally safe to assume that a print operation will complete successfully.

That is why, when we called fmt.Print, we didn’t store its return values in variables or check if an error had occurred. However, if you want to be extra safe, then you can always do that.

Printing to Any Writer With the Fprintf Function

If you look at the code for the fmt.Print function in the Go standard library, you will see that it is defined like so (at least in version 1.19 of the programming language, which I’m using):

func Print(a ...any) (n int, err error) {
	return Fprint(os.Stdout, a...)
}

This is because fmt.Fprint is a more general version of fmt.Print. Its first argument is an io.Writer, which specifies where the output will be sent. Using the os.Stdout constant as this argument will send the output to the console.

The example below shows how a file handle can be used as an io.Writer, enabling the fmt.Fprint function to write to a file:

package main

import (
	"fmt"
	"os"
)

func main() {
	tempFile, err := os.CreateTemp("", "")
	if err != nil {
		panic(err)
	}
	defer tempFile.Close()

	fmt.Fprint(tempFile, "This is", " a test")

	tempFileContent, err := os.ReadFile(tempFile.Name())
	if err != nil {
		panic(err)
	}

	fmt.Print(string(tempFileContent))
}

The os.CreateTemp function produces a new file in the os.TempDir directory, returning a handle to it.

We can then use this handle as the first argument of the fmt.Fprint function, allowing us to print content to it, just as we had to the console previously.

Finally, we read the content of the file, simply in order to check that it’s been printed correctly, and output it to the console with the regular fmt.Print function.

Using the Println Function

The fmt.Println function is very similar to the fmt.Print function except that it also emits a newline after printing its arguments to the console.

The three statements within the main function shown below therefore each produce exactly the same output:

package main

import "fmt"

func main() {
	fmt.Println("example")

	fmt.Print("example", "\n")
	
	fmt.Print("example" + "\n")
}

A backslash followed by the letter 'n' is how a newline is written in Go strings. This escape sequence is necessary because ordinary Go strings, enclosed in double quotes, do not span multiple lines.

However, it is also possible to write a multiline string by enclosing it in backticks instead of double quotes. This form of string does not interpret any escape sequences.

Introducing the Printf Function

Anyone who’s used the C programming language will understand immediately what the fmt.Printf function does — and how extremely powerful it can be — because calling the printf function (defined in the <stdio.h> header file) is the main way that output is printed to the console in C.

The fmt.Printf function always takes at least one argument, which is called the format string, but it also takes an arbitrary number of optional arguments. The format string determines how the optional arguments should be printed.

Understanding the Format String Used in the Printf Function

The format string is just a collection of runes — in other words, characters — like any other string, most of which will be printed out exactly as they’re written, but it also contains placeholders that will be replaced by one of the optional arguments.

Placeholders are shown by the '%' character followed by another character, which is known as a verb, that indicates the type of the value that will be printed to the console.

For example, the "%s" placeholder will be replaced by a string, since the 's' character stands for the word string. On the other hand, the "%d" placeholder will always be replaced by an integer, since the 'd' character stands for the word decimal, which is how the number will be printed, in base 10.

Matching Arguments In the Printf Function

Both the number and type of the optional arguments passed to the fmt.Printf function must match the placeholders in the format string.

Let’s consider an example: if you were to pass the format string "my name is %s and I am %d years of age" to the fmt.Printf function, you would also need to pass two more arguments, the first being a string and the second an integer, because that’s what the placeholders specify.

If you were to pass two integers as the second and third arguments instead, the fmt.Printf function would fail to function correctly. It would not convert the first integer to a string: it would have expected you to do that already (perhaps by using the strconv.Itoa function), since you specified that you were going to be passing a string.

If, on the other hand, you were to pass "Marco" and 42 as the second and third arguments with the same format string, then the fmt.Printf function would replace the two placeholders and print the following string:

my name is Marco and I am 42 years of age

You can see how elegant the use of the format string was, allowing us to work with values of different types before converting them all to one long concatenated string and printing it out to the console.

Looking at Some Verbs Used in the Printf Function

As we have discussed above, the verbs used in the format string specify the type of the value that will be printed by the fmt.Printf function.

There are a great number of verbs available. However, it’s not necessary to memorize them all, because only a few will be used most often. Below is a list of the mostly commonly used verbs:

VerbType
"%s"string
"%t"boolean
"%d"integer, represented in the base-10 (decimal) number system
"%b"integer, represented in the base-2 (binary) number system
"%o"integer, represented in the base-8 (octal) number system
"%x"integer, represented in the base-16 (hexadecimal) number system
"%f"floating-point number
"%v"any

It is particularly worth remembering that the 'b' character in "%b" stands for the word “binary”, and not “boolean”.

On the other hand, the 't' in "%t" stands for “true or false”.

If in doubt about which verb to use, just remember that you can always use "%v", and it will print out a sensible default representation of any value.

Setting Precision in the Printf Function

In current versions of the Go programming language, floating-point numbers will be printed with a default precision of six. In other words, six digits will follow the decimal point, even if that means lots of unnecessary zeros are shown.

However, it is possible to print a floating-point number with a set degree of precision. For example, the code below prints the value of the mathematical constant pi to four decimal places, automatically rounding up the number as necessary:

package main

import "fmt"

func main() {
	const pi = 3.14159265

	fmt.Printf("%.4f\n", pi)
}

You can see that placing the ".4" between the percentage symbol and the verb is what told the function to print the number only to four decimal places.

Therefore, if we had placed ".2" between the percentage symbol and the verb, it’s easy to conclude that the number would have been printed with a precision of two decimal places instead.

Setting Width in the Printf Function

By default, fmt.Printf will use only as much space as is necessary to print a value.

However, there are times when you might want to align values in columns, ensuring that each takes up a minimum amount of space.

To demonstrate that, I’ve created a somewhat real-world example below, involving the presentation of results for a class of students based on their performance in exams:

package main

import "fmt"

type SchoolGrade struct {
	Name   string
	Scores []int
}

func (grade SchoolGrade) AverageScore() float64 {
	var sum float64

	for _, score := range grade.Scores {
		sum += float64(score)
	}

	return sum / float64(len(grade.Scores))
}

func main() {
	grades := []SchoolGrade{
		{Name: "Xiaoming", Scores: []int{86, 78, 84, 87}},
		{Name: "Jonathan", Scores: []int{56, 68, 66, 82}},
		{Name: "Petunia", Scores: []int{91, 88, 96, 93}},
		{Name: "Anil", Scores: []int{85, 80, 64, 89}},
		{Name: "Roderick", Scores: []int{74, 60, 78, 70}},
	}

	fmt.Println("       NAME      SCORE")
	fmt.Println(" ========== ==========")

	for _, grade := range grades {
		fmt.Printf(
			" %10s %10.2f\n",
			grade.Name,
			grade.AverageScore(),
		)
	}
}

The SchoolGrade struct contains some basic information about each student in the class, just a name and a collection of the student’s examination results.

There is also a simple method called AverageScores which adds up all of a student’s exam results and divides the sum by the number of exams, in order to return an score that represents the average performance across all exams.

Within the main function, we have some dummy data for an imaginary set of students.

The interesting part, however, comes when we iterate through the data, printing a homemade table with two columns, one for each student’s name and the other for each student’s average exam result.

Running the program should produce the following output:

       NAME      SCORE
 ========== ==========
   Xiaoming      83.75
   Jonathan      68.00
    Petunia      92.00
       Anil      79.50
   Roderick      70.50

If we look at the first placeholder in the format string that we pass to the fmt.Printf function within the for loop, we can see that by placing the number 10 between the percentage symbol and the 's', we stipulated that we wanted to print a string with a minimum width of ten characters, using spaces to pad as necessary. This is what aligns our columns so neatly.

The second placeholder stipulates both a width of ten and a precision of two, so the floating-point number that represents a student’s average score is always printed to two decimal places, and those decimal places are included when the function works out how much is required padding.

Since, in the example above, each of the average scores requires two digits followed by a decimal point and two more digits, each of the numbers is padded with five space characters, in order to bring the total width up to ten.

Looking at Some Flags Used in the Printf Function

Flags are special characters that modify verbs in some way.

(If you wanted to be clever and continue the analogy with the English language, you could think of them as adverbs, since adverbs are words that modify verbs in English grammar — and maybe that would have been a better name for them. It would also have reduced the risk of the confusion with command-line flags, which are something completely different.)

Each flag is placed after the '%' character and before the verb in the format string.

Below is a table listing four flags that the fmt.Printf function can make use of:

FlagPurpose
"#"adds a prefix (for example, "0x" before hexadecimal strings)
"+"always prints a positive or negative sign before numbers
"-"aligns the value towards the left when adhering to a width, which must also be specified
"0"pads numbers with zeroes, instead of spaces, when adhering to a specified width

Using the Printf Function With Flags

The following example shows a very simple example of flags in action:

package main

import "fmt"

func main() {
	number := 42

	fmt.Printf("The number is %+08d\n", number)
}

If we read the placeholder in the format string from left to right, starting at the percentage symbol, then we can scan our eye towards the final 'd' character, which is our verb, and see that we are expecting an integer to be printed in a base-10 representation.

In between the percentage symbol and the 'd' character are the flags and the width specifier.

The character '+' tells us that we always want to see a plus or minus printed, depending on whether the number is positive or negative — so since our number is positive, we’ll see a plus.

The number zero ensures that any padding will be done with zeroes, rather than spaces, and the number eight tells us that we want the width of the printed number to be at least eight-characters long, in this case including the plus sign.

So running the program will print the following output to the console:

The number is +0000042

Concluding Thoughts About Print Functions

We have now looked at the four most useful functions for printing to the console in Go.

When we just want to print a simple string to the console, we should generally use the fmt.Println function, since it’s good practice always to emit a newline character after any other text.

However, when we want to handle the presentation of a number of values of differing types before printing them out to the console, we should rely on the fmt.Printf function, which can be a much more complicated thing to work with. However, it is also a real workhorse of a function that allows us to arrange the presentation of our output precisely, once we have acquired a good understanding of the syntax used in its format string.

Leave a Reply

Your email address will not be published. Required fields are marked *