Go Programming Series: This lesson is part of our comprehensive Complete Go Tutorial: Learn Go (Golang) from Beginner to Advanced.
Go Input and Output: fmt, Reading Input and Printing Output
Input and output connect a Go program to the outside world. Console programs read values from users, print results, report errors, and format data. The fmt package is the first tool most Go beginners use for this work, but Go's I/O model extends much further through io, bufio, os, files, network connections, and HTTP.
This guide focuses on standard console input and output, formatting verbs, scanning values, reading lines, handling errors, and choosing an appropriate I/O approach.
Learning I/O here also prepares you for file handling and web development later in the Go learning journey.
What You Will Learn
- fmt.Println and fmt.Print
- fmt.Printf
- Formatting verbs
- fmt.Scan and fmt.Scanln
- bufio.Scanner
- Reading from os.Stdin
- Handling input errors
- Writing to stdout and stderr
1. What Is Input and Output?
Input is data entering a program; output is information leaving it. In command-line programs, input commonly comes from standard input and output goes to standard output. Errors and diagnostics may be written to standard error.
2. Printing with fmt.Println
fmt.Println("Hello, Go!")
fmt.Println("Score:", 95)
3. Printing Without a Newline
fmt.Print("Enter your name: ")
4. Formatted Output with fmt.Printf
name := "Asha"
age := 22
fmt.Printf("Name: %s, Age: %d\n", name, age)
5. Common Formatting Verbs
| Verb | Typical Use |
|---|---|
| %s | String |
| %d | Integer |
| %f | Floating-point |
| %t | Boolean |
| %v | Default representation |
| %T | Value's type |
6. Formatting Examples
value := 42
fmt.Printf("value=%d type=%T\n", value, value)
7. Reading Input with fmt.Scan
var name string
fmt.Scan(&name)
8. Reading Multiple Values
var name string
var age int
fmt.Scan(&name, &age)
9. Checking Scan Errors
var age int
if _, err := fmt.Scan(&age); err != nil {
fmt.Println("Invalid input:", err)
}
10. fmt.Scanln
fmt.Scanln can read values until a newline, but it is not always the best choice for full-line user input. When spaces and complete lines matter, bufio.Scanner or another buffered approach is often clearer.
11. Reading Lines with bufio.Scanner
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Enter a sentence: ")
if scanner.Scan() {
fmt.Println(scanner.Text())
}
12. Scanner Errors
After scanning, check scanner.Err() when the input source can fail. Console examples often omit this for brevity, but production code should consider error handling.
13. Standard Input, Output and Error
fmt.Fprintln(os.Stdout, "normal output")
fmt.Fprintln(os.Stderr, "diagnostic output")
14. Writing to an io.Writer
Many Go APIs accept io.Writer, which makes code easier to test and reuse. Instead of hard-coding stdout everywhere, a function can accept a writer and send its output there.
15. Testable Output
func greet(w io.Writer, name string) {
fmt.Fprintf(w, "Hello, %s!\n", name)
}
16. Input Validation
User input is untrusted data. Validate ranges, formats, required fields, and parsing errors before using the values in calculations or application logic.
17. Practical Console Program
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Name: ")
if scanner.Scan() {
name := scanner.Text()
fmt.Printf("Hello, %s!\n", name)
}
}
18. Common I/O Mistakes
- Ignoring scan errors
- Using Scan when complete lines are required
- Mixing scanning approaches without understanding buffered input
- Printing errors to normal output
- Building business logic directly around terminal input instead of separating input from processing
19. I/O Selection Guide
| Need | Useful Tool |
|---|---|
| Simple console output | fmt.Println / fmt.Print |
| Formatted output | fmt.Printf / fmt.Fprintf |
| Simple token input | fmt.Scan |
| Full input lines | bufio.Scanner |
| File or generic output | io.Writer |
| Standard error | os.Stderr |
20. Frequently Asked Questions
What package is used for basic Go input and output?
The fmt package is commonly used for formatted console I/O.
How do I print a variable?
Use fmt.Println, fmt.Printf, or another formatting function.
How do I read a string with spaces?
A line-oriented approach such as bufio.Scanner is usually more appropriate than fmt.Scan.
Why should I check input errors?
Input can be missing, malformed, or otherwise invalid. Error handling prevents invalid values from silently entering the program.
What is os.Stderr used for?
It is the standard error stream, commonly used for diagnostics and error messages.
21. Input Handling at Application Boundaries
Console input is a small example of a broader production problem: external data is untrusted and arrives as text or bytes. Experienced Go developers separate reading, parsing, validation, and business logic. This keeps malformed input from leaking into deeper layers and makes the core application easier to test.
Separate read from parse
A useful design is to read a line first and then parse it. This gives the program control over whitespace, empty input, error messages, and retry behavior. It also avoids mixing input handling with business operations.
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Age: ")
if !scanner.Scan() {
return
}
age, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil {
fmt.Fprintln(os.Stderr, "invalid age:", err)
return
}
Scanner limits and large input
bufio.Scanner has a default token-size limit. For ordinary console lines this is usually sufficient, but programs processing very large lines should configure a larger buffer or choose bufio.Reader or another streaming approach. Input strategy should match expected data size.
When fmt.Scan is appropriate
fmt.Scan is convenient for small, whitespace-delimited exercises and command-line demonstrations. It becomes less convenient when the program needs full-line input, detailed validation, custom error messages, or precise control over delimiters. Choose the simplest tool that matches the input contract.
22. Output Design: Human, Machine and Error Channels
Program output has different audiences. Human-facing messages can be readable, while machine-consumed output should have a stable format. Diagnostic messages should generally go to stderr rather than being mixed with data sent to stdout. This separation matters when command-line programs are composed into pipelines.
fmt.Fprintln(os.Stdout, "result:", result)
fmt.Fprintln(os.Stderr, "warning: using default configuration")
Formatting is part of the interface
Once users or scripts depend on command output, formatting becomes an interface. Changing labels, delimiters, or line structure can break downstream consumers. For automation, consider structured formats such as JSON when appropriate.
23. I/O and Testability
Hard-coding os.Stdout or os.Stdin throughout business logic makes testing harder. A cleaner design passes an io.Reader or io.Writer into functions that need input or output. This allows tests to provide bytes.Buffer values without interacting with the terminal.
func greet(w io.Writer, name string) error {
_, err := fmt.Fprintf(w, "Hello, %s!\n", name)
return err
}
Error handling and partial input
Input can fail halfway through a workflow because a user sends EOF, a pipe closes, or a reader returns an error. Robust code checks read and parse errors instead of assuming every read succeeds. For long-running tools, decide whether one bad record should stop processing or be reported and skipped.
24. Input Validation Patterns
Validation should usually occur after parsing into the correct type. First establish that the input is syntactically valid, then check domain constraints. For example, “abc” is a parsing failure for an integer, while “-1” may parse correctly but violate a rule that the value must be positive.
age, err := strconv.Atoi(strings.TrimSpace(input))
if err != nil {
return fmt.Errorf("age must be a number: %w", err)
}
if age < 0 || age > 120 {
return fmt.Errorf("age must be between 0 and 120")
}
25. I/O Decision Guide for Go Applications
Use fmt for straightforward formatted input and output, bufio for buffered or line-oriented work, io.Reader and io.Writer for reusable components, os.Stdin/Stdout/Stderr for process-level streams, and structured encoders when another program will consume the output. The best choice is driven by the contract, size, and testability requirements.
26. I/O Edge Cases Worth Testing
Test empty input, leading and trailing whitespace, invalid numbers, very long lines, EOF, malformed UTF-8 when relevant, redirected stdin, output-write failures, and multiple records. These cases distinguish a demonstration program from a dependable command-line tool.
27. Command-Line Programs as Composable Tools
A well-designed command-line program can be used interactively or composed with other programs. Keep normal results on stdout, diagnostics on stderr, return meaningful exit codes, and avoid adding decorative output that makes machine consumption difficult.
Structured output
When another program will consume the result, JSON or another explicit format is often more reliable than human-oriented text. Keep the schema stable and document changes that could break scripts.
enc := json.NewEncoder(os.Stdout)
if err := enc.Encode(result); err != nil {
fmt.Fprintln(os.Stderr, "encode error:", err)
}
28. I/O Performance and Streaming
For large inputs, avoid loading the entire source into memory when streaming is sufficient. Buffered readers and writers can reduce system-call overhead, while streaming parsers keep memory usage bounded. The right approach depends on input size, latency, and whether the operation requires random access.
29. I/O Production Checklist
Define the input format, validate every external value, handle EOF and read errors, separate stdout from stderr, make output stable when it is machine-consumed, inject readers and writers for tests, and test large or malformed input. These practices turn basic fmt knowledge into dependable I/O design.
30. I/O as a Boundary Between Your Program and the Outside World
Input and output code sits at the edge of a program, where assumptions meet reality. Files can be truncated, users can enter malformed data, network streams can end early, and output destinations can fail. The fmt package makes simple programs easy to write, but production I/O requires deliberate choices about buffering, validation, error propagation and ownership of resources.
The key design principle is to separate acquisition from interpretation. Read enough data to establish the boundary, validate it, convert it into domain values, and then let the rest of the program work with typed data. This prevents parsing details from leaking into business logic and makes failure behavior easier to test.
31. Worked Production Scenario
A command-line utility may accept a configuration file from stdin. Instead of passing raw scanner tokens throughout the application, the program can read the input, decode it into a configuration structure, validate required fields and then pass the validated structure to the service layer. The I/O boundary becomes a controlled adapter rather than a source of global assumptions.
func readConfig(r io.Reader) (Config, error) {
data, err := io.ReadAll(r)
if err != nil {
return Config{}, fmt.Errorf("read config: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return Config{}, fmt.Errorf("decode config: %w", err)
}
if err := cfg.Validate(); err != nil {
return Config{}, fmt.Errorf("validate config: %w", err)
}
return cfg, nil
}
The important lesson is not the exact implementation above. It is the separation of concerns around the language feature. The code validates assumptions at the boundary, keeps state local, returns useful failure information, and leaves the core operation simple enough to test. In a larger repository, the same pattern should be adapted to the package contracts and data model rather than copied mechanically.
32. Deeper Engineering Considerations
For larger inputs, streaming is often the more important design decision than the choice of printing function. A bufio.Scanner is convenient for token-oriented input but has token-size behavior that must be considered; bufio.Reader or io.Reader-based APIs may be more appropriate for larger or binary streams. Experienced code chooses the abstraction based on the data contract rather than using the first convenient API.
31. Testing the Topic at the Boundary
A useful test suite does not simply prove that the happy-path example compiles. It asks what happens at the edges: empty input, zero values, maximum practical values, invalid states, repeated operations and failure paths. For language fundamentals, these tests are especially valuable because they reveal where an intuitive mental model differs from Go's actual semantics.
When the topic is used inside a larger function, test the behavior that depends on it rather than testing syntax in isolation. A comparison article should test the decision it drives; an assignment article should test the resulting state; an I/O article should test malformed input and partial failure. This keeps tests aligned with user-visible behavior.
32. What Experienced Reviewers Look For
A senior reviewer usually asks a different question from a beginner. Instead of asking whether the syntax is legal, the reviewer asks whether the code communicates ownership, failure behavior, invariants and intent. The language feature is only the mechanism; the quality of the surrounding design determines whether the mechanism is being used well.
Reviewers also look for accidental complexity. If a feature can be expressed with a smaller, clearer construct, the simpler version usually wins. Go's ecosystem strongly rewards code that another developer can understand quickly, format consistently and debug without reconstructing hidden conventions.
33. Practical Code-Review Checklist
- Verify that the code compiles under the project's declared Go toolchain and that gofmt has been applied.
- Check zero values, nil behavior and boundary conditions rather than reviewing only representative values.
- Look for unnecessary scope, hidden shared state and variable shadowing.
- Confirm that errors are handled at the layer that can make a meaningful decision.
- Prefer explicit domain intent over clever expressions or compressed syntax.
- Check whether tests exercise failure paths and important edge cases.
- Ask whether the chosen representation will remain understandable after the code grows.
Conclusion
Go's basic I/O tools are easy to start with but scale into a broader io model. Learn fmt for formatting, Scanner for line-oriented input, error handling for unreliable input, and io.Writer for reusable code. These skills form a useful bridge from console programs to files and network applications.
Practical Study Notes
This go input and output: fmt, reading input and printing output article should be used as a working reference while practicing Go. Do not learn the rules only by reading them. Reproduce the examples, change the inputs, deliberately create a compiler error, and then fix it. That workflow builds stronger understanding than memorizing syntax.
A Useful Practice Cycle
- Read the rule and identify the exact search intent of the page.
- Type the example yourself rather than copying it blindly.
- Change one value or condition and predict the result before running the program.
- Create one invalid version and read the compiler error.
- Rewrite the example in a slightly different form.
- Use the concept in a small real program before moving to the next topic.
Troubleshooting Checklist
- Read the compiler error from the first reported problem downward.
- Check the package and import declarations first.
- Check the types of the values involved in the failing expression.
- Run gofmt after changing source structure.
- Use small examples to isolate a confusing rule.
- Consult package documentation when the problem involves a library API.
- Do not solve a simple language problem by adding an unnecessary dependency.
Where This Topic Fits in the Go Learning Journey
Cluster 2 moves from source-code structure into the core rules used by everyday Go programs. The learning order is intentional: syntax comes first, then comments and variables, followed by types and conversions, operators, and finally practical input/output. After these concepts are comfortable, the next stage can introduce conditional statements, loops, range, break/continue, and recursion.
Next Learning Step
Continue with Conditional Statements in Go. The next page should deepen the concept rather than restart the entire Go introduction.
Input and Output Practice and Edge Cases
Write a console program that reads a name and prints a formatted greeting using fmt.Printf.
Read an integer with fmt.Scan and deliberately enter invalid text. Handle the returned error.
Read a full sentence with bufio.Scanner and explain why token-oriented scanning is different.
Write a function that accepts io.Writer and test it with a buffer instead of printing directly to the terminal.
Send normal program output to os.Stdout and diagnostics to os.Stderr.
Separate input parsing from business logic so the calculation can be tested without requiring interactive terminal input.
| Self-check | What good understanding looks like |
|---|---|
| Explain the rule | You can describe it without copying the article. |
| Write an example | You can produce a small working program from memory. |
| Spot a failure | You can predict at least one common compiler/runtime mistake. |
| Apply it | You can use the concept in a small practical program. |
Additional Editorial and Practical Notes
Separate reading input from processing it whenever practical. This lets the processing logic be tested with ordinary function calls instead of requiring interactive terminal input.
Formatted output is part of a user-facing contract. Stable labels, clear units, and appropriate formatting make command-line programs easier to understand and automate.
When input comes from users, treat conversion failures as normal possibilities rather than exceptional surprises. A robust program explains the problem and asks for valid input or returns a useful error.
Use writers and readers as abstractions when a function needs reusable I/O behavior. This also makes unit tests easier because a bytes.Buffer can stand in for a terminal or file.
Do not let fmt calls become scattered through business logic in a larger application. Keep presentation near the command-line layer and keep core functions focused on data and behavior.
Conclusion Check
Before moving on, make sure you can explain the main rule in your own words, write a small example without copying it, identify at least one common mistake, and describe how the concept behaves when the input or type changes. That is the minimum practical understanding expected from this stage.