Intermediate

I/O Operations in Go

Learn console input and output, formatted printing, and reading and writing files in Go with practical Docker-ready examples

Input and output are how a program talks to the outside world — the terminal, files, and other processes. Go handles I/O through a small set of well-designed packages built around simple interfaces (io.Reader and io.Writer) that compose cleanly across the standard library.

The fmt package covers formatted console I/O, bufio provides buffered reading and writing for efficiency, and the os package exposes files and the standard streams (os.Stdin, os.Stdout, os.Stderr). A defining feature of Go’s I/O is explicit error handling: functions that can fail return an error value that you are expected to check, rather than throwing exceptions.

In this tutorial you’ll go beyond the single fmt.Println from Hello World to learn the format verbs, read user input from the terminal, and write and read files — always with idiomatic Go error handling.

Formatted Output

Go’s fmt package offers several ways to print. Println adds spaces and a newline, Print does neither, and Printf uses format verbs (%s, %d, %f, and more) for precise control. Sprintf returns the formatted result as a string instead of printing it.

Create a file named formatted_output.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

func main() {
	name := "Ada"
	age := 37
	pi := 3.14159

	// Println adds spaces between operands and a trailing newline
	fmt.Println("Hello,", name)

	// Print adds no newline and no spaces between string operands
	fmt.Print("Age: ")
	fmt.Print(age)
	fmt.Print("\n")

	// Printf uses format verbs for precise control
	fmt.Printf("%s is %d years old\n", name, age)
	fmt.Printf("Pi rounded: %.2f\n", pi)
	fmt.Printf("Hex: %x, Binary: %b\n", 255, 5)

	// Sprintf returns a formatted string instead of printing it
	greeting := fmt.Sprintf("Welcome, %s!", name)
	fmt.Println(greeting)
}

Common verbs: %s (string), %d (integer), %f (float, with %.2f for precision), %t (boolean), %v (any value in its default format), and %T (the value’s type). Running formatted_output.go prints:

Hello, Ada
Age: 37
Ada is 37 years old
Pi rounded: 3.14
Hex: ff, Binary: 101
Welcome, Ada!

Reading Input from the Terminal

To read what a user types, wrap os.Stdin in a bufio.Scanner. Each call to Scan() reads one line, and Text() returns it (without the trailing newline). This is the idiomatic way to read line-based input in Go.

Create a file named read_input.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)

	fmt.Print("Enter your name: ")
	scanner.Scan()
	name := scanner.Text()

	fmt.Print("Enter your age: ")
	scanner.Scan()
	age := scanner.Text()

	fmt.Printf("Hello, %s! You are %s years old.\n", name, age)
}

Because this program reads from standard input, you supply the answers when you run it. Piping in Ada and 37 produces:

Enter your name: Enter your age: Hello, Ada! You are 37 years old.

The two prompts appear back-to-back because piped input is not echoed to the terminal the way interactive typing is. When run interactively, you would see each prompt, type a response, and press Enter.

Writing and Reading Files

File I/O showcases Go’s error-handling style: every operation that can fail returns an error alongside its result, and you check it before proceeding. The example below writes three lines to a file using a buffered writer, reads the whole file back at once with os.ReadFile, then reads it again line by line with a scanner.

Create a file named file_io.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	// Create (or truncate) a file for writing
	file, err := os.Create("notes.txt")
	if err != nil {
		fmt.Println("Error creating file:", err)
		return
	}

	// Buffer writes for efficiency, then flush before closing
	writer := bufio.NewWriter(file)
	lines := []string{"First line", "Second line", "Third line"}
	for _, line := range lines {
		fmt.Fprintln(writer, line)
	}
	writer.Flush()
	file.Close()
	fmt.Println("Wrote 3 lines to notes.txt")

	// Read the entire file into memory at once
	data, err := os.ReadFile("notes.txt")
	if err != nil {
		fmt.Println("Error reading file:", err)
		return
	}
	fmt.Print(string(data))

	// Reopen and read line by line
	f, err := os.Open("notes.txt")
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer f.Close()

	fmt.Println("Reading line by line:")
	scanner := bufio.NewScanner(f)
	lineNum := 1
	for scanner.Scan() {
		fmt.Printf("%d: %s\n", lineNum, scanner.Text())
		lineNum++
	}
}

A few things to note:

  • os.Create opens a file for writing, creating it or truncating an existing one.
  • bufio.NewWriter buffers writes; call Flush() to ensure buffered data reaches the file before you close it.
  • fmt.Fprintln writes to any io.Writer (here the buffered writer) — the same formatting family as Println, redirected to a destination.
  • defer f.Close() schedules the close to run when main returns, a common Go idiom for cleanup.
  • os.ReadFile is the simplest way to read an entire small file; the scanner approach suits large files you want to process one line at a time.

Running with Docker

The file_io.go example is fully self-contained — it creates its own file and reads it back — so it runs the same way every time.

1
2
3
4
5
# Pull the official image
docker pull golang:1.23

# Run the file I/O example
docker run --rm -v $(pwd):/app -w /app golang:1.23 go run file_io.go

To try the interactive input example, pipe answers into standard input:

1
2
# Feed two lines of input to read_input.go
printf 'Ada\n37\n' | docker run --rm -i -v $(pwd):/app -w /app golang:1.23 go run read_input.go

Expected Output

Running file_io.go produces:

Wrote 3 lines to notes.txt
First line
Second line
Third line
Reading line by line:
1: First line
2: Second line
3: Third line

The notes.txt file is created in your working directory (mounted into the container via $(pwd)), so it persists after the container exits.

Key Concepts

  • io.Reader and io.Writer are the two interfaces at the heart of Go I/O — most I/O functions accept or return them, so components compose freely.
  • fmt verbs (%s, %d, %.2f, %v, %T) give precise control over formatting; Sprintf and Fprintln reuse the same formatting for strings and arbitrary writers.
  • Errors are values — I/O functions return an error you check explicitly (if err != nil), rather than throwing exceptions.
  • bufio buffers I/O for efficiency: bufio.Scanner reads input line by line, and bufio.Writer batches writes (remember to Flush()).
  • os.Create, os.Open, and os.ReadFile cover the common file operations; pair Open/Create with defer file.Close() to guarantee cleanup.
  • Standard streams os.Stdin, os.Stdout, and os.Stderr are just *os.File values you can read from or write to like any other file.

Running Today

All examples can be run using Docker:

docker pull golang:1.23
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining