skip to content

In Go, what type are os.Stdin, os.Stdout and os.Stderr, and what belongs on each?

level: juniorimportance: must knowfreq 74%

answer

  1. three files the process starts with
  2. descriptors 0, 1 and 2
  3. *os.File variables, not constants
  4. data down one, humans down the other

basics

~20 s

os.Stdin, os.Stdout and os.Stderr are package-level *os.File variables wired to file descriptors 0, 1 and 2. A program's real output goes to stdout; warnings, progress and errors go to stderr, so a pipeline consumes only data.

solid answer

~40 s

They are ordinary `*os.File` values declared as variables in the `os` package, opened on descriptors 0, 1 and 2, so anything wanting an `io.Reader` or `io.Writer` accepts them directly. `fmt.Println` writes to `os.Stdout`; you reach the other stream explicitly with `fmt.Fprintln(os.Stderr, ...)`, and the `log` package's default logger already writes there. The split matters for a tool that ends up inside somebody's shell pipeline: stdout carries the machine-readable result the next command parses, stderr carries everything a human reads. A progress message printed to stdout becomes the downstream program's input, and a user who redirects stdout to a file still wants to see the errors. Because they are variables rather than constants they can be reassigned, which tests sometimes exploit, though taking an `io.Writer` parameter is the cleaner design.

code

go · 8 lines
go
n, err := process(input)
if err != nil {
	// human-facing: stays on the terminal even if stdout is redirected
	fmt.Fprintln(os.Stderr, "process:", err)
	return err
}
// machine-facing: this is what the next command in the pipeline parses
fmt.Println(n)

go deeper

for a junior

Be ready to name the type (*os.File), the three descriptor numbers, and say which stream a normal result versus a warning goes to. Know that fmt.Println targets os.Stdout and that stderr must be named explicitly.

for a middle

Explain what the shell does to each stream under redirection and piping, and why a diagnostic on stdout corrupts the next command's input. Mention that these are package variables and can be swapped.

for a senior

Show the judgment of an author whose binary ends up in other people's scripts: keep stdout strictly machine-readable, put everything human on stderr, and take an io.Writer in library code rather than reaching for the globals.

for a principal

The tradeoff you own is the tool's output contract. Once scripts parse your stdout its shape is an interface you cannot change quietly, so decide early what counts as data, what counts as diagnostics, and whether machine-readable output is a separate flag.

## What they actually are In Go the three standard streams are three package-level variables in `os`: - `os.Stdin` — file descriptor 0, the process's input - `os.Stdout` — file descriptor 1, its normal output - `os.Stderr` — file descriptor 2, its diagnostic output All three have type `*os.File`. That single fact answers most questions about them. `*os.File` implements `io.Reader`, `io.Writer` and `io.Closer`, so the streams can be handed to anything in the standard library that accepts those interfaces — `fmt.Fprintf`, `io.Copy`, `bufio.NewWriter`, `encoding/csv`, `encoding/json` — with no adapter at all. They are not special runtime objects and there is no hidden buffering layer between them and the kernel: a `Write` on `os.Stdout` is a `write` system call on descriptor 1. They are also *variables*, not constants. `os.Stdout = f` compiles and works. This is process-global mutable state, so it is a tool of last resort, but it is why a test can redirect a package's printed output without changing that package. ## Who points them where The program does not open these descriptors; it inherits them. The shell decides what they are attached to before the process starts: - `tool` — all three are the terminal - `tool > out.txt` — descriptor 1 becomes the file; 0 and 2 are untouched - `tool | other` — descriptor 1 becomes the write end of a pipe whose read end is `other`'s descriptor 0 - `tool 2> log` — descriptor 2 becomes the file; the result on stdout is unaffected This is the whole reason the split exists. Redirection and piping act on stdout by default and leave stderr alone, so the convention is fixed for you: **stdout is data, stderr is for humans.** ## What belongs on stdout Only the thing the program was asked to produce, in a form another program can consume: the converted file, the matched lines, the JSON document, the computed number. Nothing else. Every extra byte you print there is a byte the next command in the pipeline has to parse. `tool | jq .` fails outright if a friendly banner precedes the JSON, and `tool > data.csv` silently poisons the file with a `processing...` line. ## What belongs on stderr Everything else a person might read: errors, warnings, progress counters, verbose logging, usage text on a bad flag. Two properties make stderr the right home. First, the shell leaves it attached to the terminal even when stdout is redirected, so the user still sees why their command failed while the output goes to a file. Second, Go never buffers it, so a message written immediately before a crash is already on the screen. ```go n, err := process(input) if err != nil { fmt.Fprintln(os.Stderr, "process:", err) return err } fmt.Println(n) ``` `fmt.Println`, `fmt.Printf` and `fmt.Print` are shorthand for the `Fprint*` forms with `os.Stdout` as the destination. There is no `fmt.Eprintln`; reaching stderr always means naming it. ## Reading from stdin `os.Stdin` is the mirror image: an `*os.File` you can hand to `bufio.NewReader`, `io.Copy`, `encoding/json.NewDecoder` or anything else expecting an `io.Reader`. A filter reads it, transforms, and writes the result to stdout — which is what lets your program be dropped into the middle of somebody else's pipeline without knowing anything about its neighbours. ## Common mistakes - Printing diagnostics with `fmt.Println` out of habit, so they land in the data stream. - Assuming Go buffers stdout the way C's stdio does. It does not; each write goes straight out. - Believing stderr is only for fatal errors. It is for anything human-facing, including routine progress. - Reaching for `os.Stdout` deep inside a library. A library should take an `io.Writer` (or return the bytes) and let the program decide where output goes. ## The practical rule Write it down as a contract before the first release: what appears on stdout is an interface other people's scripts will depend on, and once they do you cannot change its shape quietly. Everything conversational goes to the other descriptor.

  • Why does writing a progress message to os.Stdout break a tool used in a pipeline?
    Whatever you print to stdout becomes the next command's input. `tool | jq .` fails because a progress line is not JSON, and `tool > out.csv` writes the message into the data file. Progress is for a human, so it belongs on `os.Stderr`, which the shell leaves pointed at the terminal even when stdout has been redirected.
  • Can you reassign os.Stdout, and when would you?
    Yes - they are package variables, so `os.Stdout = f` works, and tests sometimes use it to capture output. It is process-global and racy under parallel tests, and any code that already stored the original `*os.File` keeps writing to it. Taking an `io.Writer` parameter makes the same code testable without touching global state.
  • Is anything written to os.Stderr buffered by Go?
    No. `os.Stderr` is an unbuffered `*os.File`, so each `Write` is one syscall and the message reaches the terminal immediately - exactly what you want for a warning or a crash. Wrapping stderr in a `bufio.Writer` for speed takes on the obligation to flush it, and a message written just before the program dies may then never appear.

Stdout is the conveyor belt handing parts to the next machine; stderr is the operator's intercom. Anything you say on the belt ends up inside the next machine.

saying these in an interview costs you the question

  • Says Go buffers os.Stdout the way C's stdio does
  • Prints warnings and progress to stdout in a pipeline tool
  • Thinks os.Stdin is an io.Reader interface value, not an *os.File
  • Believes stderr is only for fatal errors, never warnings or progress
  • Assumes the three streams are constants that cannot be replaced
  • Has a library function print to os.Stdout instead of a passed io.Writer