In Go, what does `defer f.Close()` on a file you wrote to silently discard?
answer
- the call returns something you ignored
- the kernel taking bytes is not storing them
- a wrapper around the file has no Close
- os.WriteFile checks it; your code did not
basics
~20 sClose returns an error and a deferred call discards it. On a file you wrote to, that error can be the only report that your data never reached the disk, so a failed write ends up looking like a success.
solid answer
~50 s`Close` on an `*os.File` returns an `error`, and `defer f.Close()` evaluates the call purely for its side effect and throws the result away. That matters on the write path because a `Write` can be accepted by the kernel and still fail later — a full disk noticed at writeback, a quota, a network filesystem flushing on close — and the close is where that failure is first reported. There is a second, more common loss with the same shape: `bufio.Writer` has no `Close` method, so if you wrap the file and forget `Flush`, closing the file never pushes the last buffer out. Either way the function returns nil and the process exits 0 with a short file. The fix is to check it: the standard library's own `os.WriteFile` reports the close error whenever the write itself did not already fail.
code
go · 13 linesfunc writeReport(path string, rows []byte) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close() // the close error is dropped here
w := bufio.NewWriter(f)
if _, err := w.Write(rows); err != nil {
return err
}
return w.Flush()
}go deeper
Be ready to say out loud that Close returns an error and that a deferred call throws it away. Know that on a file you wrote to, that error may be the only sign the data did not land.
Explain the mechanics: the kernel can accept a write and fail at writeback, and a bufio.Writer holds bytes the file never sees. Be able to name Flush, Sync and Close as three different guarantees.
Show how you would catch this in real code — checking the close error on every write path, and choosing whether Sync is part of the job's contract before you declare a run successful.
Frame it as a data-integrity risk rather than a style nit: a silent short write costs more than a crash. Be ready to argue for a build-time check on unchecked cleanup errors in write paths.
### Close returns an error for a reason Everything closable in Go satisfies `io.Closer`, whose entire definition is `Close() error`. Writing `defer f.Close()` is legal — a deferred call may discard its results — but it means the one value the call produces is dropped on the floor. On a handle you only read from, that value is rarely actionable. On a handle you wrote to, it is frequently the *only* place a failure will ever be reported. ### Why a write can succeed and the close still fail `os.File.Write` is a thin wrapper over the operating system's `write`. The kernel accepting your bytes is not the same as the bytes being stored, and several ordinary failures are only discovered afterwards: - **Delayed allocation.** On filesystems such as ext4, blocks may not be allocated at write time. A full disk or an exceeded quota then surfaces during writeback, and the close can be the first call in a position to observe it. - **Network filesystems.** NFS clients famously buffer and flush on close; a `close()` returning a no-space or quota error is the canonical example, and Go surfaces it as the error from `f.Close()`. - **Pipes, sockets and devices exposed as files**, where the peer going away is noticed at teardown. The rule is the same one C programmers learned the hard way: check the result of closing anything you wrote to. ### The second loss: a buffer that has no Close `bufio.Writer` has `Write`, `Flush`, `Buffered` and `Reset` — it has **no** `Close` method at all. Wrapping a file with `bufio.NewWriter(f)` and deferring only `f.Close()` means the final partial buffer is never handed to the file descriptor. The program exits 0 and the output is short by up to one buffer's worth of bytes, which is why this bug is usually noticed as a row-count mismatch rather than as an error. The same trap appears with wrappers that *do* have a `Close`: a `compress/gzip.Writer` writes its footer in `Close`, and closing the underlying file does not close the gzip writer. An `encoding/csv.Writer` needs `Flush` and then a check of its `Error` method. Closing the file never reaches back up the chain — each layer must be finished in its own right, innermost first. ### Flush, Sync and Close are three different guarantees - `w.Flush()` moves bytes from a user-space buffer into the file descriptor. - `f.Sync()` asks the operating system to push them to stable storage; it is the fsync call. - `f.Close()` releases the descriptor and reports whatever the filesystem reports at that point. If your job's contract is 'the file is durably on disk before I record the run as complete', `Close` alone does not give you that; `Sync` before `Close` does, and both errors have to be checked. If the contract is only 'the data reached the OS', `Flush` plus a checked `Close` is enough. ### What to write instead The minimal correct shape is the one the standard library uses in `os.WriteFile`: keep the write error, and let the close error take its place only when there is no write error yet. ```go _, err = f.Write(data) if cerr := f.Close(); cerr != nil && err == nil { err = cerr } return err ``` When the writes happen further down the function, the same effect is achieved with a named result and a deferred closure that assigns into it. On a read-only handle, `defer f.Close()` remains fine — and writing `defer func() { _ = f.Close() }()` makes the discard deliberate rather than accidental, which is what a reviewer wants to see. ### Why this defect survives so long There is no panic, no log line, and no non-zero exit status. The only symptom is data that is quietly incomplete, discovered days later by whoever reconciles counts. That asymmetry — cheap to write correctly, expensive to detect — is the reason interviewers use `defer f.Close()` as a code-review question.
- If Write already returned nil, what is actually left for Close to fail on?The kernel accepting bytes is not the same as storing them. With delayed allocation, blocks are assigned during writeback, so a full disk or an exhausted quota can surface after every `Write` returned nil. Network filesystems buffer and flush at close for the same reason. `Close` is the last point at which the filesystem gets to tell you the file is not what you think it is.
- Does closing an os.File flush a bufio.Writer that wraps it?No. The wrapper holds bytes in its own buffer in your process; the file knows nothing about it. `bufio.Writer` has no `Close` method, so you must call `Flush` yourself and check its error before closing the file. Layered writers finish innermost first: flush or close the wrapper, then close the file.
- Is Close enough to say the file is safely on disk?No. `Close` releases the descriptor and reports pending filesystem errors, but it does not fsync. If the requirement is surviving a machine crash, call `f.Sync()` before `Close` and check both errors. If the requirement is only that the bytes reached the operating system, a flushed writer plus a checked `Close` is sufficient.
Deferring Close on a file you wrote to is like handing over a payment and walking out without looking at the receipt — the one piece of paper that would have told you the transaction was declined.
saying these in an interview costs you the question
- Close cannot fail once the writes returned nil
- closing the file flushes any writer wrapping it
- Close does an fsync, so the data is durable
- ignoring Close is fine because the process is exiting anyway
- a short output file must mean the input was short