skip to content

Streams and Buffering

Bytes move through Go as io.Reader and io.Writer values you wrap: bufio for buffering, io.MultiWriter and io.Pipe for plumbing, io.ReadFull for short reads. The traps here lose data silently.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

30

A bufio.Writer wraps an output file and the last records never appear on disk - why, and what fixes it?

level: juniorimportance: must knowfreq 62%

answer

  1. the bytes are still in memory
  2. a bufio.Writer has no Close method
  3. closing the file does not reach the wrapper
  4. deferred calls run last-in-first-out
  5. Flush is also where the write error appears

basics

~20 s

A bufio.Writer holds bytes in memory until its buffer fills, so whatever is still buffered when the program ends is simply lost. Call Flush before the file is closed, and check the error Flush returns.

solid answer

~40 s

`bufio.NewWriter` gives you a 4 KiB in-memory buffer. Each `Write` or `WriteString` copies into that buffer and only touches the underlying file when the buffer fills or when you call `Flush`. A `bufio.Writer` has no `Close` method and the `*os.File` knows nothing about the wrapper, so closing the file does not push the tail out - those bytes are dropped when the process exits. The fix is an explicit `w.Flush()` whose error you actually return, with `defer f.Close()` registered first so it runs after the flush (deferred calls run last-in-first-out). Flush is also where a failed underlying write first surfaces: `defer w.Flush()` throws that error away, which is how a disk-full failure becomes a silently short file.

code

go · 12 lines
go
f, err := os.Create(path)
if err != nil {
	return err
}
defer f.Close() // registered first, so it runs last
w := bufio.NewWriter(f)
for _, rec := range records {
	if _, err := w.WriteString(rec + "\n"); err != nil {
		return err
	}
}
return w.Flush() // the tail leaves here, and so does any write error

go deeper

for a junior

Be ready to say in one sentence what Flush does: it pushes bytes the writer is still holding into the file underneath. Know that closing the file does not do it for you and that the wrapper has no Close.

for a middle

Explain the mechanics: a 4 KiB default buffer, writes that only reach the file when it fills, deferred calls running last-in-first-out, and Flush being the first place a failed underlying write shows up.

for a senior

Show the shape you actually ship - an explicit Flush whose error is returned, a Close ordered after it, and a clear statement of what Flush guarantees versus what fsync guarantees. Expect to be asked about concurrent writers.

for a principal

Treat the buffer size as a data-loss budget rather than a performance knob: how many records the team can afford to lose on a hard kill, whether the answer is a smaller buffer, a timed flush or an fsync, and what throughput each choice costs.

## What a bufio.Writer actually is `bufio.NewWriter(w)` returns a `*bufio.Writer` that owns a byte slice - 4096 bytes by default, or whatever you ask for with `bufio.NewWriterSize`. It exists to turn many small writes into few large ones. Writing a 60-byte log line straight to an `*os.File` is a `write` syscall per line; routed through a `bufio.Writer`, roughly seventy lines are copied into memory and leave as one syscall. That is the whole trade: **your data now lives in user-space memory that nothing else knows about**. ## Why the tail disappears Three facts combine into the bug: 1. `Write` and `WriteString` return `nil` as soon as the bytes are copied into the buffer. A successful return says nothing about the file. 2. `bufio.Writer` has **no `Close` method**. There is nothing for a `defer` to close. 3. The `*os.File` underneath has no idea it is being buffered. `f.Close()` closes a file descriptor; it cannot reach into a wrapper it has never heard of. So a program that writes 10,000 records and returns has flushed only the full buffers along the way. The final partial buffer - up to 4 KiB of records - is in a slice that goes out of scope, and the file ends mid-record. It looks exactly like a truncated file, which is why people go looking for a disk problem or a producer problem rather than at the missing `Flush`. ## The correct shape ```go f, err := os.Create(path) if err != nil { return err } defer f.Close() w := bufio.NewWriter(f) for _, rec := range records { if _, err := w.WriteString(rec + "\n"); err != nil { return err } } return w.Flush() ``` Two details matter. First, **order**: `defer f.Close()` is registered before the flush happens, and deferred calls run last-in-first-out, so the file is still open when `Flush` runs. If you write `defer w.Flush()` after `defer f.Close()`, the flush runs first - correct order, but the error is discarded. Second, **the error**: returning `w.Flush()` directly is the cheapest way to make a write failure visible. ## Where write errors live Because the underlying write is deferred, so is the error. If the disk fills while a full buffer is being pushed out, that error is recorded on the writer and returned by **every later** `Write`, `WriteString` and `Flush` call. The writer never retries and never silently drops the error, but it also never tells you at the point the offending record was produced. One check on the final `Flush` is enough to detect the failure; attributing it to a specific record is not possible without flushing more often. ## Flush is not durability `Flush` moves bytes from the `bufio.Writer` into the underlying writer's `Write` method. For an `*os.File` that means a `write` syscall, which hands the bytes to the kernel's page cache. If the machine loses power a second later, the data can still be gone. Durability is `f.Sync()` (an fsync), it is far more expensive than `Flush`, and its error deserves the same check. A candidate who says "Flush guarantees the data is on disk" has conflated two different layers. ## What to do instead of forgetting - Write the `Flush` at the same moment you write the `bufio.NewWriter` - never later. - Prefer `return w.Flush()` (or `if err := w.Flush(); err != nil {...}`) over `defer w.Flush()`. - If you must defer it, defer a closure that captures a named result: `defer func() { if err == nil { err = w.Flush() } }()`. - Treat the buffer size as the amount of output you are willing to lose if the process is killed. The same reasoning applies to any buffered sink in the standard library - a `csv.Writer` and a compressing writer each hold state that only their own `Flush`/`Close` releases. `bufio.Writer` is just the one you meet first.

  • Does bufio.Writer.Flush guarantee the records are durable on disk?
    No. Flush only copies the buffered bytes into the underlying writer's Write method, which for an *os.File is a write syscall into the kernel's page cache. A power loss can still lose them. Durability needs f.Sync(), which is much more expensive and whose error also has to be checked.
  • What happens to later writes after an underlying write fails part-way through a Flush?
    The bufio.Writer remembers the first error and returns it from every subsequent Write, WriteString and Flush. Nothing is retried and nothing is silently swallowed, but the failure is reported far from the record that triggered it, so a single check on the final Flush is what makes it visible at all.
  • Is a bufio.Writer safe to share between goroutines?
    No. It has no internal locking, so concurrent Write or Flush calls race on the buffer and its index. Give each goroutine its own writer, or serialise every Write and Flush behind one mutex - including a flush triggered from a timer.

It is a shipping crate, not a conveyor belt: records pile up inside until the crate is full or you deliberately send it. Locking the warehouse door does not ship a half-full crate.

saying these in an interview costs you the question

  • Says closing the underlying file flushes the bufio.Writer
  • Believes the buffer is flushed automatically at program exit
  • Uses defer w.Flush() and never notices the discarded error
  • Claims Flush makes the data durable on disk
  • Thinks a successful Write means the bytes reached the file
open as a page

What does io.Copy(dst, src) do in Go, and what do its two return values mean?

level: juniorimportance: must knowfreq 75%

basics

~20 s

io.Copy streams bytes from a source reader to a destination writer through one small reusable buffer, so memory does not grow with the payload. It returns an int64 count of bytes written and the error that stopped the transfer.

open as a page

Why does io.Reader's Read method take a caller-supplied []byte instead of returning the bytes it read?

level: juniorimportance: must knowfreq 72%

basics

~20 s

io.Reader.Read writes into a byte slice the caller supplies, so the caller decides how much memory a stream costs and can reuse one slice for every call. Returning a fresh slice per read would allocate on every call.

open as a page

What does io.ReadAll do, and why is it risky on a stream you do not control?

level: juniorimportance: must knowfreq 52%

basics

~20 s

io.ReadAll reads an io.Reader to end of stream and returns every byte in one slice. It takes no size limit, so a sender that keeps sending can grow that slice until the process runs out of memory.

open as a page

In io.Seeker, what do the whence values io.SeekStart, io.SeekCurrent and io.SeekEnd mean?

level: juniorimportance: must knowfreq 52%

basics

~20 s

The whence value says what the offset is measured from: io.SeekStart from the beginning of the file, io.SeekCurrent from the position you are at now, io.SeekEnd from the end. Seek returns the new absolute offset.

open as a page

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

level: juniorimportance: must knowfreq 74%

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.

open as a page

How do you write an io.Writer wrapper that counts bytes without touching call sites?

level: middleimportance: must knowfreq 52%

basics

~20 s

Declare a struct holding the inner io.Writer plus a counter, and give it a pointer-receiver Write that forwards the slice, adds the returned n to the counter, and returns that same n and error unchanged. Callers keep passing an io.Writer.

open as a page

In Go's io package, what do io.MultiReader and io.MultiWriter each produce?

level: juniorimportance: should knowfreq 48%

basics

~10 s

io.MultiReader returns a Reader that reads its sources one after another and reports EOF only after the last one ends. io.MultiWriter returns a Writer that sends every write to all of its destinations.

open as a page

When do you reach for bufio.Reader's ReadString or Peek instead of bufio.Scanner?

level: middleimportance: should knowfreq 44%

basics

~10 s

Use bufio.Scanner for ordinary token-at-a-time reading with a size cap. Use bufio.Reader when you need the delimiter kept, no token ceiling, a look at the next bytes without consuming them, or rune-by-rune decoding.

open as a page

A bufio.Scanner loop stops partway through a log file with no error - what limit causes that?

level: middleimportance: should knowfreq 52%

basics

~20 s

A line longer than the scanner's maximum token size, 64 KiB by default, makes Scan return false with bufio.ErrTooLong stored in Err. A loop that never calls Err after Scan sees that failure as a clean end of input.

open as a page

How does io.TeeReader let you hash a stream while something else consumes it?

level: middleimportance: should knowfreq 40%

basics

~20 s

io.TeeReader(r, w) returns a Reader that writes every byte it reads from r into w before returning it. Pass a hash as w and the digest builds itself as the consumer reads - but only over bytes actually read.

open as a page

What does io.Pipe() give you in Go, and how do the PipeReader and PipeWriter ends synchronise?

level: middleimportance: should knowfreq 52%

basics

~20 s

io.Pipe returns a connected PipeReader and PipeWriter with no buffer between them: each Write blocks until reads have consumed it. The two ends must run in different goroutines, and it adapts writer-based producers to reader-based consumers.

open as a page

Why must an io.Writer implementation not retain or modify the caller's slice p after Write returns?

level: middleimportance: should knowfreq 46%

basics

~20 s

The io package requires that Write neither keep the caller's slice p after returning nor modify its contents. Callers reuse one slice for an entire stream, so a retained slice is overwritten underneath the implementation, corrupting its data.

open as a page

How does io.LimitReader report that its byte limit was reached, and why is that a trap?

level: middleimportance: should knowfreq 33%

basics

~20 s

io.LimitReader reports its cap as a plain end of stream, exactly as a genuine ending looks, with no distinct error. Oversized input is therefore silently truncated rather than rejected, which is dangerous for anything you then parse.

open as a page

Why use io.ReadFull instead of one Read call on an io.Reader, and what errors does it return?

level: middleimportance: should knowfreq 44%

basics

~20 s

One Read on an io.Reader may fill only part of the slice and still return a nil error, so an exact-length read must loop. io.ReadFull is that loop: nil when full, io.EOF if it read nothing, io.ErrUnexpectedEOF if it read part.

open as a page

Why can many goroutines call os.File.ReadAt on one handle but not Seek then Read?

level: middleimportance: should knowfreq 44%

basics

~20 s

ReadAt takes the offset as an argument and neither reads nor moves the file's cursor, so parallel calls cannot disturb each other. Seek then Read is two operations over one shared cursor, and concurrent callers interleave them.

open as a page

Why can't you rewind an io.Reader, and how do you tell if a stream supports it?

level: middleimportance: should knowfreq 58%

basics

~20 s

io.Reader declares only Read. It has no position and no memory of what it handed out, so there is nothing to go back to. Only implementations that also satisfy io.Seeker or io.ReaderAt can re-read, which you check with a type assertion.

open as a page

How can a Go program tell that os.Stdout is a terminal rather than a pipe or a file?

level: middleimportance: should knowfreq 40%

basics

~10 s

Call os.Stdout.Stat() and inspect the returned mode: fi.Mode()&os.ModeCharDevice != 0 means stdout is a character device, which is what a terminal looks like. A pipe reports os.ModeNamedPipe and a redirected file reports neither.

open as a page

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

level: middleimportance: should knowfreq 56%

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.

open as a page

A relay streaming uploads through io.Pipe leaks a goroutine per request. Why, and how do you fix it?

level: seniorimportance: should knowfreq 42%

basics

~10 s

The producing goroutine is blocked writing to a PipeWriter nobody will read, because the consumer abandoned the PipeReader. Closing the PipeReader makes that Write return io.ErrClosedPipe so the goroutine can finish.

open as a page

An io.Writer returns (len(p), nil) though only part of p reached the destination. What breaks?

level: seniorimportance: should knowfreq 38%

basics

~20 s

io.Writer's contract requires a non-nil error whenever Write returns n < len(p). Reporting len(p) after a partial write makes every caller, including io.Copy, believe the bytes landed, so data is lost silently and the operation reports success.

open as a page

A heap profile of your Go frame reader shows one 900 MB []byte built from a peer's declared length. What went wrong?

level: seniorimportance: should knowfreq 29%

basics

~20 s

The code passed a peer-supplied length prefix straight to make, so the peer chose the allocation size: a few bytes on the wire bought 900 MB of heap. Validate the declared length against a maximum before allocating.

open as a page

A Go tool's progress counter shows in a terminal but never when piped into another command. Why?

level: seniorimportance: should knowfreq 34%

basics

~20 s

The counter is being written to os.Stdout. When the output is piped, the shell replaces descriptor 1, so those lines become the next command's input rather than reaching the screen. Human-facing progress belongs on os.Stderr.

open as a page

When does io.CopyBuffer beat io.Copy in Go, and what makes io.CopyBuffer panic?

level: middleimportance: nice to knowfreq 28%

basics

~10 s

io.CopyBuffer takes the scratch buffer as a parameter, so code copying many streams can reuse one instead of letting every io.Copy call allocate a fresh 32 KB. A non-nil buffer of length zero panics.

open as a page

A bufio.Writer around your output stream delayed records by minutes - when does buffering cost more than it saves?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Buffering trades latency, memory and a crash-loss window for fewer syscalls. It pays when writes are small and frequent; it costs more than it saves on slow streams, on many concurrent streams, and over sinks that already batch.

open as a page

One destination in an io.MultiWriter starts failing mid-transfer. What happens to the others?

level: seniorimportance: nice to knowfreq 26%

basics

~10 s

The fan-out stops at the failing destination and returns its error, so destinations after it never get those bytes and the whole transfer aborts. A short write with no error becomes io.ErrShortWrite.

open as a page

Concurrent lookups on one *os.File return each other's bytes, yet go test -race is clean. Why, and how do you fix it?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Seek and Read are two operations over one file cursor shared by every goroutine holding that handle, so calls interleave and each reader lands on someone else's position. The race detector watches Go memory, not that cursor, so it reports nothing. Read positionally with os.File.ReadAt.

open as a page

How do you capture what a Go function writes to os.Stdout in a test using os.Pipe?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

os.Pipe returns a connected read/write *os.File pair. Point the os.Stdout variable at the write end, drain the read end from another goroutine, then close the writer and restore os.Stdout. Without concurrent draining the test deadlocks.

open as a page

How do you decide whether a codec package accepts io.Reader and whether it returns an io.ReadCloser, knowing other teams will implement and depend on those interfaces permanently?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Accept the narrowest interface the code actually uses, usually io.Reader, and return a Closer only when your package owns a resource the caller cannot release itself. Exported interfaces cannot gain methods once outside teams implement them.

open as a page

How do you choose the maximum inbound message size for a Go service when a caller team's traffic already exceeds your candidate cap?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Derive the cap from worst-case memory: the per-message limit times the messages in flight must fit the process budget. Measure real payload sizes, shadow-log what a candidate cap would reject, and move outliers to streaming rather than raising the ceiling.

open as a page