The stdin stream
You are building a command-line tool that processes a list of tasks. The user pastes a block of text, hits enter, and your program needs to parse each line. You cannot just read one character and guess the structure. You need a reliable way to grab text from the terminal, handle the buffering, and stop exactly when the user is done.
Standard input, or stdin, is a stream of bytes. In Go, os.Stdin is an *os.File that implements the io.Reader interface. That interface gives you a Read([]byte) (int, error) method. You could call Read directly, but that forces you to manage buffers, track offsets, and handle partial reads manually. The standard library provides higher-level tools to abstract that work.
The two main tools are bufio.Scanner and bufio.Reader. Scanner splits the stream into tokens, usually lines. Reader gives you low-level access to the buffer, letting you read until a specific byte or condition. Both handle the underlying Read calls and buffering for you.
Minimal example: Scanner
The most common pattern for reading user input is bufio.Scanner. It wraps os.Stdin, buffers the input, and splits it into lines automatically.
Here is the standard loop: create a scanner, iterate while lines exist, and check for errors after the loop ends.
package main
import (
"bufio"
"fmt"
"os"
)
// main reads lines from stdin and prints them.
func main() {
// Scanner wraps Stdin and buffers reads for efficiency.
scanner := bufio.NewScanner(os.Stdin)
// Scan advances to the next token. It returns false on EOF or error.
for scanner.Scan() {
// Text returns the current token as a string.
// The newline character is stripped automatically.
fmt.Println(scanner.Text())
}
// Check for read errors after the loop terminates.
// This catches issues like permission denied or disk full.
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
}
}
Scanner handles lines. Reader handles bytes. Pick the granularity you need.
How the loop works
The Scan method does three things. First, it checks if there is data in the internal buffer. If the buffer is empty, it calls Read on the underlying os.Stdin to fill the buffer. This blocks until the user types something and hits enter, or until the stream closes.
Second, Scan runs a split function to find the next token. The default split function is ScanLines, which looks for a newline character. It returns the token and advances the internal position.
Third, Scan returns true if a token was found. If the stream ends (EOF) or an error occurs, it returns false. The loop exits.
After the loop, you must call scanner.Err(). Scan returns false for both EOF and errors. EOF is normal; an error is not. Err() tells you the difference. If Err() returns nil, the loop ended because the input finished. If it returns an error, something went wrong during reading.
Go makes you check errors explicitly. The boilerplate if err != nil is verbose by design. It forces you to acknowledge the unhappy path. Trust the pattern.
Realistic example: Interactive prompt
Scanner is great for processing a batch of lines. It is less ideal for an interactive prompt where you want to show a message, wait for one line, and react immediately. For that, bufio.Reader gives you more control.
bufio.Reader provides ReadString(delim byte). It reads until it finds the delimiter, returns the string including the delimiter, and handles buffering. You get the raw bytes, so you must trim the newline yourself.
Here is a loop that asks for input until the user types "quit".
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
// main runs an interactive loop until the user types quit.
func main() {
// Reader wraps Stdin with a buffer for efficient reading.
reader := bufio.NewReader(os.Stdin)
for {
// Prompt the user for input.
fmt.Print("Enter a command (quit to exit): ")
// ReadString blocks until it finds the newline character.
// It returns the text including the newline.
line, err := reader.ReadString('\n')
if err != nil {
fmt.Fprintln(os.Stderr, "read error:", err)
break
}
// TrimSpace removes the newline and any surrounding whitespace.
// This normalizes input like " quit " to "quit".
command := strings.TrimSpace(line)
// Check for the exit condition.
if command == "quit" {
fmt.Println("Exiting.")
break
}
fmt.Printf("You entered: %s\n", command)
}
}
Reader gives you bytes. Scanner gives you tokens. Use Reader when you need to control the delimiter or handle partial lines.
The blocking trap
Both Scanner.Scan and Reader.ReadString block. They wait for input. If you run this code in a goroutine, the goroutine blocks until the user types. If the user never types, the goroutine leaks.
This is fine for a simple CLI tool where main waits for input. It is dangerous in a server or a long-running process. If you read stdin in a background goroutine, you must have a way to cancel the read.
The standard way to cancel a blocking read is to close the file descriptor or use a non-blocking terminal mode. Closing os.Stdin is rarely the right move in a shared process. For terminal applications, use golang.org/x/term to switch the terminal to raw mode, or use a select statement with a channel that signals cancellation.
Context is plumbing. Run it through every long-lived call site. If a read can block, it needs a cancellation path.
Scanner limits and customization
Scanner has a default maximum token size of 64KB. If a line exceeds this size, Scan returns false and Err returns bufio.Scanner: token too long. This prevents a malicious user from flooding your memory with a single massive line.
You can increase the limit using scanner.Buffer. You provide a byte slice, and Scanner uses it as the buffer.
// Allocate a 1MB buffer for large lines.
buf := make([]byte, 0, 1024*1024)
scanner.Buffer(buf, len(buf))
You can also change how Scanner splits input. The default is lines. You can split by words, runes, or a custom function.
// Split by whitespace instead of newlines.
scanner.Split(bufio.ScanWords)
ScanWords splits on any sequence of whitespace. It skips empty tokens. This is useful for parsing space-separated values.
Scanner is opinionated. It assumes tokens are separated by delimiters. If your input format is complex, Scanner might fight you.
Reader vs Scanner: Memory and control
Scanner allocates a new string for every token. If you read a million lines, you allocate a million strings. The garbage collector handles this, but it adds pressure.
Reader reuses its internal buffer. ReadString returns a string that references the buffer. If you store that string, you must be careful. The next read might overwrite the buffer. Copy the string if you need to keep it.
line, _ := reader.ReadString('\n')
// Copy the string to keep it safe from buffer reuse.
safeLine := string([]byte(line))
This is a common pitfall. If you append line to a slice and keep reading, all entries in the slice might point to the same buffer content. Copy the data if you need to retain it.
Scanner is safer for simple line processing. Reader is faster for high-throughput parsing where you manage memory manually.
Pitfalls and errors
The compiler rejects programs with unused imports. If you import bufio but don't use it, you get imported and not used. Remove the import.
If you forget to check scanner.Err(), you might treat an error as EOF. Your program could exit silently when a read fails. Always check the error after the loop.
If you use fmt.Scan instead of bufio, you get a different experience. fmt.Scan skips whitespace and parses types. It is convenient for simple input like fmt.Scan(&name, &age). It is less flexible for raw text processing. fmt.Scan also blocks and has no buffer control.
The worst goroutine bug is the one that never logs. If a read fails, log it. If the input is malformed, report it.
Decision matrix
Use bufio.Scanner when you need to process a stream of lines or words. It handles buffering and splitting automatically. It is the standard choice for CLI tools and log processing.
Use bufio.Reader when you need low-level control over the buffer. It is better for interactive prompts, custom delimiters, or high-performance parsing where you manage memory reuse.
Use fmt.Scan when you need to parse structured input like numbers or strings separated by whitespace. It is convenient for simple scripts but lacks buffer control.
Use io.ReadAll when you need to read the entire input into memory at once. It is simple but unsafe for large inputs. Only use it for small, trusted data.
Use plain sequential code when you don't need concurrency. The simplest thing that works is usually the right thing.
Stdin blocks. Design your concurrency around that fact.