How to Print Output in Go

fmt.Println, Printf, and Sprintf

Use fmt.Println for simple output, fmt.Printf for formatted printing, and fmt.Sprintf for formatted string creation in Go.

Debugging with fmt

You're writing a Go program and you need to see the value of a variable. You type print(x) out of habit from Python or JavaScript. The compiler stops you with undefined: print. Go doesn't have a built-in print statement. Everything lives in the fmt package. You import fmt, call fmt.Println, and the output appears. It feels familiar, but the formatting verbs are different, the package offers more than just printing, and there are specific patterns for errors and performance that separate beginners from experienced Go developers.

The format string model

The fmt package handles formatted I/O. The name comes from "format". It provides functions to write to standard output, write to a string, write to any writer, or create errors. The core mechanism is the format string. A format string contains plain text mixed with verbs. Verbs are placeholders that start with a percent sign. You pass values as arguments, and the function substitutes them into the string.

Println converts arguments to strings, joins them with spaces, adds a newline, and writes to standard output. Printf parses the format string, matches verbs to arguments, applies formatting rules, and writes to standard output. Sprintf does the same formatting but returns the result as a string. Errorf formats a string and returns it as an error interface.

Think of Printf like a mail merge template. You write a document with blanks marked by verbs. You provide the data. The function fills the blanks and produces the final text. Sprintf is the same process, but instead of printing the document, it hands you the finished text so you can store it, return it, or pass it along. This separation lets you build strings without side effects.

Minimal example

Here's the simplest usage: print a line, format a number, and build a string.

package main

import "fmt"

func main() {
	// Println adds spaces between args and a newline at the end
	fmt.Println("Hello", "World")

	// Printf uses a format string; %d expects an integer argument
	fmt.Printf("Count: %d\n", 42)

	// Sprintf returns the formatted string without printing it
	result := fmt.Sprintf("Value: %s", "text")
	fmt.Println(result)
}

How formatting works

When you call fmt.Println, the function iterates over the arguments. It converts each value to its string representation. It joins them with a single space. It appends a newline character. It writes the result to standard output. If you pass a struct, Println uses the default formatting, which lists fields without names. This is useful for quick debugging but produces output like {Alice 30} that can be hard to read.

fmt.Printf parses the format string first. It scans for verbs like %d, %s, or %v. It matches each verb to the corresponding argument in order. It applies any flags, width, or precision specified in the verb. It writes the result to standard output. If the types don't match, the function panics at runtime. fmt.Sprintf follows the same parsing logic but writes to an internal buffer and returns the buffer as a string. No output goes to the terminal.

The %v verb is the universal fallback. It prints any value in a default format. For structs, %+v includes field names. %#v prints the value using Go syntax, which is useful for generating code or debugging literals. %T prints the type. Use %v for quick debugging. Use %+v when you need to see struct fields. Use %#v when you need the Go representation.

package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func main() {
	u := User{Name: "Alice", Age: 30}

	// %v prints default representation without field names
	fmt.Printf("Default: %v\n", u)

	// %+v includes field names for readability
	fmt.Printf("Named: %+v\n", u)

	// %#v prints Go syntax, useful for code generation
	fmt.Printf("Syntax: %#v\n", u)

	// %T prints the type of the value
	fmt.Printf("Type: %T\n", u)
}

Flags and width

Verbs can be modified with flags, width, and precision. Flags change the behavior. The - flag left-aligns the output within the width. The + flag forces a sign for numbers. The # flag uses an alternate format, like adding 0x to hex numbers. The 0 flag pads with zeros instead of spaces. The space flag leaves a blank before positive numbers to align with negative signs. You can combine flags. The order doesn't matter.

Width sets the minimum number of characters. Precision sets the number of digits after the decimal point for floats or the maximum length for strings. %10d prints an integer padded to 10 characters. %.2f prints a float with two decimal places. %10.2f prints a float with two decimals, padded to 10 characters total. Width and precision can be dynamic using * in the format string.

package main

import "fmt"

func main() {
	// - flag left-aligns within width 10
	fmt.Printf("|%-10s|\n", "hi")

	// + flag forces sign on integer
	fmt.Printf("%+d\n", 42)

	// # flag adds 0x to hex output
	fmt.Printf("%#x\n", 255)

	// * uses the next argument for width
	fmt.Printf("|%*s|\n", 10, "hi")

	// * uses the next argument for precision
	fmt.Printf("%.*f\n", 2, 3.14159)
}

Realistic usage

In production code, you rarely print raw values. You format logs, build error messages, or construct responses. Here's a realistic scenario: a function that calculates a discount and returns a formatted message. It uses Sprintf to build the return value and Printf to log the operation. It also demonstrates error wrapping with Errorf.

package main

import (
	"errors"
	"fmt"
)

// calculateDiscount returns a message describing the discount applied
func calculateDiscount(price float64, percent float64) (string, error) {
	if percent < 0 || percent > 100 {
		// %w wraps the error for inspection later
		return "", fmt.Errorf("invalid percent %f: %w", percent, errors.New("out of range"))
	}

	discount := price * (percent / 100.0)

	// Sprintf builds the return string; %.2f formats to two decimal places
	return fmt.Sprintf("Discount: $%.2f off $%.2f", discount, price), nil
}

func main() {
	// Printf logs the action with a newline
	fmt.Printf("Processing order...\n")

	msg, err := calculateDiscount(100.0, 15.0)
	if err != nil {
		// Print the error chain
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Println(msg)
}

Pitfalls and errors

The most common mistake is a mismatch between the verb and the argument type. If you use %d for a string, the program panics. The runtime error is panic: fmt: %d has invalid verb type string. Always check that the verb matches the type. Another pitfall is forgetting the newline in Printf. Println adds it automatically. Printf does not. If you omit \n, the output runs together on the same line. This makes logs hard to read. The compiler won't catch a missing newline. You have to look at the output.

A third issue is error wrapping. If you use %v to include an error in Errorf, you lose the error chain. errors.Is and errors.As won't work on the wrapped error. Always use %w to wrap errors. This preserves the chain and allows proper error inspection.

The go vet tool checks format strings. It catches mismatches between verbs and arguments at compile time. If you write fmt.Printf("%d", "string"), go vet warns you. It also catches unused arguments and mismatched counts. Always run go vet before committing. It catches these bugs early.

Performance matters in hot loops. The fmt package parses the format string at runtime. It allocates memory for the result. In a tight loop processing millions of records, fmt can become a bottleneck. For simple conversions, strconv.Itoa or strconv.FormatFloat is faster. For building complex strings, strings.Builder avoids intermediate allocations. Use fmt for logging and user output. Use specialized functions for hot paths.

Convention: The log package wraps fmt. log.Printf adds a timestamp and file location. In production code, prefer log over fmt for messages. fmt is for interactive tools and tests. log is for services. The community accepts fmt boilerplate for clarity. Don't hide errors. Print them or return them.

Decision matrix

Use fmt.Println when you need quick debugging output and don't care about precise formatting. Use fmt.Printf when you need to control the layout, such as aligning columns or formatting numbers to specific decimal places. Use fmt.Sprintf when you need to construct a string value for return, storage, or further processing without writing to output. Use fmt.Errorf when you need to create an error with context and wrap an underlying error. Use fmt.Fprint variants when you need to write to a file, buffer, or network connection instead of standard output. Use strconv functions when performance is critical in a hot loop and you only need simple type conversions. Use %v when you want a safe default representation for any type during debugging. Use %w when wrapping errors to preserve the error chain.

Println is for humans. Printf is for precision. Sprintf is for data. Trust go vet to catch your verbs. Wrap errors with %w.

Where to go next