go / atomicfile
I use atomic file writes to prevent partial or corrupt files when a program crashes or is interrupted mid-write.
The problem
A naive file write can leave a file in a corrupt state:
// Dangerous: if the program crashes mid-write,
// the file may be truncated or partially written
err := os.WriteFile("config.json", data, 0644)
Atomic write
Write to a temp file, sync to disk, then rename:
func WriteFile(filename string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(filename)
f, err := os.CreateTemp(dir, filepath.Base(filename)+".tmp")
if err != nil {
return err
}
tmpName := f.Name()
// Clean up on any error
defer func() {
if err != nil {
f.Close()
os.Remove(tmpName)
}
}()
if _, err = f.Write(data); err != nil {
return err
}
if err = f.Chmod(perm); err != nil {
return err
}
if err = f.Sync(); err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
return os.Rename(tmpName, filename)
}
Key points:
- Same directory: Create temp file in the same directory as the target
so
os.Renameis atomic (rename across filesystems copies instead). - Sync before rename:
f.Sync()flushes data to disk. Without it, a power failure could lose data even after rename. - Rename is atomic: On POSIX systems,
rename(2)atomically replaces the target file. Readers see either the old or new file, never partial.
Usage
config := Config{Debug: true}
data, _ := json.MarshalIndent(config, "", " ")
if err := WriteFile("config.json", data, 0644); err != nil {
log.Fatal(err)
}
When to use
- Config files
- State files that must be valid on restart
- Any file where partial writes would be problematic
For append-only logs or very large files, other strategies (write-ahead logs, checksums) may be more appropriate.