Never lose a byte to a crash.
If two processes write the same file, one silently wins and the other silently loses data. This makes that impossible, in three lines of Go.
go get github.com/larsartmann/go-atomic-write@latest package main
import (
"os"
atomicwrite "github.com/larsartmann/go-atomic-write"
)
func main() {
path := "/etc/app/config.json"
data, _ := os.ReadFile(path)
fp := atomicwrite.FingerprintFromBytes(data)
newData := []byte(`{"updated": true}`)
err := atomicwrite.WriteVerified(path, newData, fp)
// err == ErrConcurrentModification if someone
// else wrote between your read and write
_ = err
} Safety you can rely on.
What can go wrong in a plain file write, and what this library does about it.
No silent overwrites
A fingerprint check catches concurrent modification between your read and your write, so you get an error instead of a lost update.
Zero-cost fingerprinting
~11x faster than SHA-256, zero allocations, ~27 GB/s throughput. Memory-bound, not compute-bound.
Safe across processes
flock on Unix, LockFileEx on Windows. Protects across processes, not just goroutines.
The file is never missing
Single rename(2) on POSIX — no window where the file is missing. MoveFileEx on Windows.
Survives power loss
fsync on the temp file before rename, fsync on the directory after. Data survives power loss.
Two deliberate dependencies
Only xxhash for hashing and flock for locking. Both intentional, neither replaceable.
Four phases. No window for corruption.
Every write passes through this pipeline. If any check fails, the original file is untouched.
Fingerprint
Compute an xxhash64 digest when you read the file.
fp := atomicwrite.FingerprintFile(path) Stage + fsync
Write new content to a unique temp file, then fsync for durability.
atomicwrite.WriteVerified(path, data, fp) Lock + Verify
Acquire exclusive lock, re-read the file, verify fingerprint still matches.
// flock.Lock() + Fingerprint.Matches() Atomic Rename
Single rename(2) replaces the target. Directory is fsync'd (POSIX).
// os.Rename(tmp, path) + syncDir() Why go-atomic-write?
os.WriteFile handles none of this. A naive temp+rename handles one.
| Feature | os.WriteFile | DIY | go-atomic-write |
|---|---|---|---|
| TOCTOU-safe | |||
| Crash-durable (fsync) | |||
| Concurrent-write safe | |||
| Atomic rename | |||
| Fingerprint verification | |||
| Cross-platform locking | |||
| Dependencies | 0 | 0 | 2 |
Built for real workloads.
Drop it into any read-modify-write cycle.
Config Files
Read-modify-write cycles safe from concurrent edits
State & Checkpoints
Corruption-free persistence for databases and jobs
CI/CD Pipelines
Concurrent writers on shared artifacts and lock files
The objections, answered.
Including when this library is the wrong tool for the job.
Why not just write a temp file and rename it?
That handles crashes and missing files, but not races. If another process changes the file between your read and your write, one of the two updates is silently lost. go-atomic-write locks the file, re-reads it, and verifies the fingerprint first. On a mismatch you get ErrConcurrentModification instead of a lost update.
When should I not use this?
If one process owns the file, never crashes mid-write, and you do not need durability across reboots, plain os.WriteFile is simpler. This library earns its keep when a file is shared between processes or has to survive a crash.
What does a write cost?
One xxhash64 pass (~27 GB/s, zero allocations), one advisory lock, and one full re-read of the file while the lock is held. Negligible for config, state, and checkpoint files; measurable for multi-gigabyte files.
What happens when two writers race?
Exactly one wins. The loser gets ErrConcurrentModification and the original file is left untouched, because the temp file is discarded before any rename. Writers holding fresh data can retry; writers that do not should surface the error.
Does it work on Windows?
Yes. LockFileEx replaces flock, a failed os.Rename is retried five times with exponential backoff on ERROR_ACCESS_DENIED and ERROR_SHARING_VIOLATION (antivirus, open handles), and directory fsync is a no-op because Windows has no equivalent.
Do I have to change how I read files?
No. Reads stay as they are. You only change the write: compute a fingerprint when you read, then call WriteVerified. WriteIfChanged skips the write entirely when the bytes already match, which keeps file watchers and mtimes quiet on no-op runs.
Start writing safely.
Two functions. Zero corruption.