skip to content

Is Go's os.Stdout buffered, and what changes when you wrap it in a bufio.Writer?

level: middleimportance: should knowfreq 56%

answer

  1. Go adds no layer of its own
  2. one Write, one syscall
  3. C line-buffers to a tty; Go does not
  4. bufio.NewWriter batches into 4 KB
  5. nothing empties the tail for you

basics

~20 s

No. Every write to os.Stdout is one write syscall, with none of C stdio's automatic line buffering. Wrapping it in a bufio.Writer batches writes into 4 KB blocks, and you then owe an explicit Flush before returning.

solid answer

~40 s

`os.Stdout` is a plain `*os.File` with no buffering layer, so every `fmt.Println` becomes one `write` syscall, and Go has none of C stdio's line-buffer-to-a-terminal, block-buffer-to-a-pipe behaviour. For a filter emitting a million records that is a million syscalls, so you wrap it: `w := bufio.NewWriter(os.Stdout)`, write everything through `w`, and the bytes accumulate in a 4 KB buffer that spills when full. The cost is that the buffer is now yours to empty - nothing flushes it for you, and `bufio.Writer` has no `Close`. Call `w.Flush()` before the owning function returns and check its error, because the deferred write to descriptor 1 is where a failure actually surfaces. For a program printing a handful of lines, skip the wrapper entirely; the syscalls were never the bottleneck.

code

go · 10 lines
go
out := bufio.NewWriter(os.Stdout) // 4096-byte buffer
defer func() {
	if err := out.Flush(); err != nil {
		fmt.Fprintln(os.Stderr, "flush:", err)
	}
}()

for _, rec := range records {
	fmt.Fprintln(out, rec)
}

go deeper

for a junior

Know that writes to os.Stdout go straight out, that bufio.NewWriter(os.Stdout) exists to batch them, and that you must call Flush before the owning function returns or the last lines vanish.

for a middle

Explain the syscall-per-write cost, the 4 KB default buffer, and why Go deliberately omits C's terminal-dependent buffering. Be able to say who owns the flush and where a write error actually surfaces.

for a senior

Show you know when not to buffer, that mixing buffered and direct writes to the same stream reorders output, and that a checked Flush error is the only proof the bytes landed.

for a principal

The call you own is where buffering lives in a codebase: one writer created at the top and passed down as an io.Writer beats every layer buffering independently and arguing later about who owed the flush.

## Go's answer: no buffering at all `os.Stdout` is an `*os.File`. Its `Write` method calls `write(2)` on descriptor 1 and returns. There is no library-level buffer, and - importantly for anyone arriving from C - no behaviour that depends on what stdout is attached to. C's stdio line-buffers when stdout is a terminal and block-buffers when it is a pipe, which is why a C program's output pattern changes the moment you pipe it. Go's does not: it is the same, unbuffered, in both cases. That is a deliberate simplicity trade. You never get surprising interleaving or a lost tail by default, and you never need to reason about a mode you did not choose. What you get instead is one syscall per `Write`. ## What that costs A syscall is on the order of a microsecond. Printing one line per record with `fmt.Println` means one syscall per record, and for a filter processing millions of records the process spends most of its time in the kernel doing tiny writes. This is the single most common reason a Go CLI is slower than the equivalent shell pipeline. Note also that `fmt.Fprintf` formats into a temporary buffer and issues **one** `Write` per call, so the syscall count tracks the number of print calls, not the number of format verbs. ## Wrapping stdout ```go out := bufio.NewWriter(os.Stdout) defer func() { if err := out.Flush(); err != nil { fmt.Fprintln(os.Stderr, "flush:", err) } }() for _, rec := range records { fmt.Fprintln(out, rec) } ``` `bufio.NewWriter` allocates a 4096-byte buffer; `bufio.NewWriterSize(os.Stdout, n)` picks another size. Writes copy into the buffer; when a write does not fit, the buffer is flushed to `os.Stdout` and filling resumes. A single write larger than the whole buffer is passed straight through to the underlying writer rather than chopped up, so an undersized buffer is a performance question, never a correctness one. Syscalls now scale with total bytes divided by 4 KB rather than with the number of lines - typically two or three orders of magnitude fewer. ## What you take on **The flush is yours.** `bufio.Writer` has no `Close` method. `os.Stdout` knows nothing about your buffer. Nothing in the runtime will empty it. If the function returns without `Flush`, everything written since the last spill - up to a full buffer's worth - is simply never written. The output looks *almost* right, which is what makes it a nasty bug: the file is short by a few kilobytes at the end. **The error moves.** `bufio.Writer` records the first write error and keeps returning it; the individual `fmt.Fprintln(w, ...)` calls mostly just copy bytes and succeed. The real `write` to descriptor 1 happens on a spill or on `Flush`, so a checked `Flush` error is the only evidence your output actually landed. A disk-full or broken-pipe condition shows up there. **Ordering becomes your problem.** If some code writes through the wrapper and other code writes directly to `os.Stdout`, the two streams of bytes interleave in an order that has nothing to do with program order. Pick one writer for the stream and pass it down. ## When not to buffer - A program that prints a few lines and exits: the wrapper adds a flush obligation and buys nothing. - An interactive prompt: buffered output can leave the prompt sitting in memory while you wait on input, so a prompt written through a buffer must be flushed before the read. - `os.Stderr`: leave it unbuffered. Diagnostics are low volume and you want them out immediately. ## Sizing 4 KB matches a typical page and is fine for almost everything. Tens of kilobytes can help a very high-volume filter, with quickly diminishing returns; megabyte buffers mostly increase how much output is in limbo if the process dies. Measure with a benchmark before changing it. ## The rule to carry away Go gives you no buffering and no surprises; adding a `bufio.Writer` is a deliberate trade of one syscall per line for one flush obligation you must honour on every path out of the function that owns the writer.

  • Why does the error from bufio.Writer.Flush matter more than the errors from the writes before it?
    The individual `fmt.Fprintln(w, ...)` calls usually just copy bytes into memory and succeed. The real `write` to descriptor 1 happens when the buffer spills or when you `Flush`, and `bufio.Writer` holds the first failure in a sticky field. So a `Flush` that returns nil is the only evidence the output reached the file or pipe; a disk-full or broken-pipe error surfaces exactly there.
  • Does closing anything flush a bufio.Writer wrapped around os.Stdout?
    No. `bufio.Writer` has no `Close` method, and `os.Stdout` holds nothing of yours to flush, so closing it changes nothing. The buffered tail is lost unless you call `Flush` yourself on every path out of the function that owns the writer - which is why the flush usually sits in a deferred closure that reports its error.
  • How large is the default bufio.Writer buffer, and when would you change it?
    `bufio.NewWriter` uses 4096 bytes; `bufio.NewWriterSize(os.Stdout, n)` chooses another. Larger buffers cut syscalls on a high-volume filter with fast-diminishing returns past tens of kilobytes, and they increase how much output is unwritten if the process dies. A record larger than the buffer is written straight through, so an undersized buffer is never a correctness problem.

saying these in an interview costs you the question

  • Assumes Go line-buffers stdout when it is a terminal
  • Believes the runtime flushes buffered writers for you
  • Ignores the error returned by bufio.Writer.Flush
  • Wraps os.Stdout in bufio for a program printing three lines
  • Thinks bufio.Writer has a Close method that flushes
  • Writes some output through the wrapper and some directly to os.Stdout