Right, let’s talk about fmt. This is probably the first package you ever used in Go, printing a timid “Hello, World” to the console. But don’t let its simplicity fool you; fmt is a workhorse. It’s how you talk to your program, how you debug in a panic, and how you present data to a user. It’s also where a lot of new Gophers get their first papercut. I’m here to make sure you get the good stuff without the bloodshed.

The package is built around two core ideas: printing (sending data out) and scanning (reading data in), both heavily reliant on format verbs. These are those little % characters that tell the package how to interpret the data you’re throwing at it. Think of them as the instructions you scribble on a box before moving house. “Fragile!” (%v), “This End Up!” (%#v), “Books - Seriously, Why Do I Own This Many Books?” (%T).

The Workhorses: Print, Sprint, Fprint

First, the family of functions that end with Print. They all use the default format verbs to do their job, but they differ in where they send the output.

  • fmt.Print: Writes directly to standard output (your terminal).
  • fmt.Sprint: “String” print. It doesn’t write anywhere; it just returns a string. This is your go-to for building a string from a bunch of variables.
  • fmt.Fprint: “Writer” print. This is the most powerful one. You give it any io.Writer (a file, a network connection, a bytes buffer) and it writes there. Print and Sprint are actually just convenient wrappers around Fprint.
package main

import "fmt"

func main() {
    name := "Val"
    widgets := 42

    // Goes to stdout (your terminal)
    fmt.Print("Name: ", name, ", Widgets: ", widgets, "\n") 

    // Builds and returns a string
    s := fmt.Sprint("Name: ", name, ", Widgets: ", widgets, "\n") 
    fmt.Print(s) // Now we print the string

    // The most flexible: write to a bytes.Buffer (which implements io.Writer)
    var buf bytes.Buffer
    fmt.Fprint(&buf, "Name: ", name, ", Widgets: ", widgets, "\n")
    fmt.Print("Buffer contains: ", buf.String())
}

Notice we had to add the newline \n ourselves. That’s where the ln suffix family comes in.

The Polite Relatives: Println, Sprintln, Fprintln

These are the same as above, but they automatically add a space between operands and a newline at the end. They’re for when you want to be tidy.

fmt.Println("Name:", name, "Widgets:", widgets) // Much cleaner
// Output: Name: Val Widgets: 42

Use these for quick-and-dirty debugging. For any kind of formal output, you’ll want precise control, which is where the f suffix family enters the picture.

Precision Tools: Printf, Sprintf, Fprintf

This is where you bring out the format verbs. You provide a format string that acts as a template, and then the values to slot into that template. This is how you get consistent, human-readable output.

// %s for string, %d for decimal integer, \n for newline
fmt.Printf("Name: %s, Widgets: %d\n", name, widgets)

// Pad the integer to be exactly 6 characters wide, useful for columns
fmt.Printf("Name: %s, Widgets: %6d\n", name, widgets)

The Format Verb Zoo (The Important Ones)

This is the crucial part. Mess this up, and you get a runtime panic or gibberish. The verb must match the type of the value you’re passing.

  • %v: The “value” default format. Great for quick stuff. It’ll print a string, an int, a struct—whatever you have.
  • %#v: The “Go syntax” representation. This is your best friend for debugging. It will show you the value as it would appear in your Go code. For a string, it includes the quotes. For a struct, it shows the field names. It tells you exactly what you’re looking at.
  • %T: Prints the type of the value. fmt.Printf("%T", name) would print string.
  • %d: Decimal integer. Not for floats!
  • %f: Decimal floating-point number. You can control precision: %8.2f means 8 total characters wide, with 2 digits after the decimal point.
  • %s: The plain string. This will not safely escape arbitrary data. Remember that.
  • %q: A quoted string. It will safely escape any non-printable characters and wrap the result in double quotes. This is what you should use if you’re printing data you didn’t create yourself and you need to see what’s really in it. It’s the difference between seeing a literal newline character break your terminal and seeing \n.
  • %x: Hexadecimal. Works on strings (prints each byte as hex), integers, and byte slices.
data := "Hello\tWorld\n"
fmt.Printf("%%s:  %s\n", data)  // Looks like: Hello	World
fmt.Printf("%%q:  %q\n", data)  // Looks like: "Hello\tWorld\n"
fmt.Printf("%%+v: %+v\n", data) // The + modifier adds field names for structs

The Pitfalls: Where fmt Fights Back

  1. Number of arguments: Printf will panic if the number of verbs doesn’t match the number of arguments. Println just prints what it gets. This is a common source of runtime errors.
  2. Type mismatch: Using %s with an int is a compile-time error? Nope. The fmt package uses reflection, so these errors are only caught at runtime. You will get a panic: %!s(int=42). It’s one of the few places where Go’s type safety lets you down. Be careful.
  3. Arbitrary data in %s: Never use %s to print data from an external source (user input, a file, a network request). It could contain control characters that mess up your terminal. Use %q to see its true, escaped form, or use the strconv package to properly quote it.

Scanning: The Dark Arts of Reading Input

The Scan family of functions (Scan, Scanln, Scanf) are the inverse of the Print family. They read from standard input (by default) and parse what they find into variables you provide.

Let me be direct: for anything beyond the most trivial CLI tool, I find these functions clunky and brittle. They’re great for tutorials and one-off scripts, but for real user input, you’re often better off reading a whole line with bufio.Scanner and parsing it yourself with strconv.

Why? Because Scanln will break on any whitespace, making it impossible to read a string with a space in it without resorting to Scanf with a convoluted format string.

var name string
var score int

// This will break if the user enters "Val Smith"
fmt.Print("Enter your name and score: ")
count, err := fmt.Scanln(&name, &score)
if err != nil {
    fmt.Fprintf(os.Stderr, "Scan failed: %v\n", err)
    // Handle error - probably ask them to try again
}
fmt.Printf("Hi %s, your score is %d\n", name, score)

The Scanf function requires you to know the exact format of the input, including any punctuation. It’s powerful but inflexible.

// User MUST type: "Name: Dave, Score: 100"
fmt.Scanf("Name: %s, Score: %d", &name, &score)

My advice? Use the Scan functions for quick hacking. For production, take the time to read a line and parse it properly. You’ll thank yourself later. The fmt package is brilliant at output, but for input, it often leaves you wanting more.