Don't defer Close() on writable files (2017)
Core issue: defer Close on writable files
defer f.Close()in Go discards theCloseerror, so write failures that only surface at close time are silently ignored.- Several commenters stress that finalizers (
defer, destructors,Drop) are for cleanup, not for operations that can meaningfully fail. - A common pattern suggested: defer a best-effort
Closepurely to avoid leaks, but also callClose(andSync) explicitly at the point where correctness depends on success.
Go patterns and tooling
- Idioms discussed:
- Explicit sequence for writes:
Write→Sync→Close, all checked. - Defer a helper like
CheckClose/Capturethat only records aCloseerror if no earlier error occurred. - Use multi-error aggregators (
errors.Join, third‑partymultierr) to surface both the main error and a close error.
- Explicit sequence for writes:
- Some warn that deferring error-producing functions can hide ordering bugs: side effects may happen after a failed close, leaving inconsistent state.
- Double-closing:
- OS
close(2)can be dangerous to retry (FD reuse race). - Go’s
os.File.Closeis explicitly idempotent and masks the syscall after first use, so “defer then explicit close” is safe at the Go level.
- OS
Exceptions vs explicit errors
- Debate is intense:
- Pro-exceptions: simpler “happy path”; errors propagate automatically; constructs like try-with-resources / using / context managers can attach suppressed close errors; easier to catch at appropriate abstraction level.
- Pro-explicit errors: forces thinking about failures at call sites; improves locality and maintainability; exceptions make control flow and failure modes harder to see; large ecosystems show widespread misuse of generic catch‑all handlers.
- Some argue modern “checked-like” sum types (Rust
Result, Swift) give exception-like ergonomics with explicitness and better performance. - Others claim Go’s
(T, error)is just “errno with sugar” and not as strong as real sum types; defenders reply that multiple return values and interface-typed errors are significantly better than a globalerrno.
Other languages and RAII/finalizers
- Rust: files auto-close in
Drop, but close errors are ignored;sync_allmust be called explicitly for durability. There were proposals for ownership-consumingclose(self)but it’s complex and unresolved. - C++/Java: destructors /
finallycan’t safely throw when another exception is in flight; languages handle double-faults poorly (terminate, swallow original, etc.). Try-with-resources / suppressed exceptions improve but don’t eliminate the issue. - C#, Python:
using/withmanage disposal; errors inDispose/__exit__generally surface as exceptions, sometimes replacing the original error but may be accessible via “suppressed” metadata. - Zig:
closeintentionally ignores most errors (exceptEBADF), reinforcing the view that resource release should be infallible; explicitflush/syncis where correctness lives.
Filesystem semantics, fsync, and durability
- Discussion broadens to the difficulty of writing correct, durable file code on POSIX:
close()success does not guarantee data is on disk; writeback is buffered.- For new files, you often must
fsyncthe file and its parent directory to ensure the directory entry is durable. fsyncfailure is tricky: buffers may be invalid; retries may wrongly appear to succeed.- Ext4 “zero-length file” and
fsyncgateare cited as examples where naive “write+rename” patterns broke under legal-but-surprising reordering.
- Some argue most applications shouldn’t call
fsyncat all (performance, battery life; complexity better handled at lower layers). - Others counter that
fsyncis the only portable way to enforce ordering/consistency across crashes, and that many apps get this wrong; serious workloads should consider using SQLite or a DB rather than ad‑hoc file formats.
High-level takeaways and disagreement
- Broad agreement:
- Don’t ignore write/close errors you actually care about.
defer/RAII are great for leak prevention, weak for correctness‑critical failure reporting.- File and filesystem APIs are full of subtle, system-dependent edge cases.
- Disagreement:
- Whether Go’s explicit error style is a net win over exceptions.
- How often
Close/fsyncerrors matter in real-world apps versus being “theoretical”. - Whether problems are primarily language-level (error handling model) or OS/filesystem-level (APIs and semantics).