Why write a config file to a temp file and os.Rename it onto the target instead of overwriting in place?
answer
- two states only, never a half state
- a reader may open it at any instant
- build the new one under another name
- swapping a name is one operation
- os.CreateTemp beside the target, then os.Rename
basics
~20 sOverwriting in place leaves the file half-written, so a concurrent reader can load a truncated config. Writing a complete temp file first and calling os.Rename swaps the name in one step: readers see the whole old file or the whole new one.
solid answer
~40 sRewriting a file in place is not one event: the target is emptied and then filled, and anything that opens it in between reads a partial document. On a POSIX filesystem `os.Rename` is a single directory-entry operation, so the path flips from the old contents to the new contents with nothing observable in between. The idiom is: `os.CreateTemp` in the **same directory** as the target, write the full contents, `f.Sync()`, `f.Close()` (checking its error), then `os.Rename(tmpName, target)`, with a `defer os.Remove(tmpName)` to clean up if any step fails. The temp file must sit in the target's directory because a rename cannot cross filesystems. This is what lets a controller rewrite a sidecar's config while the sidecar may re-read it at any moment.
code
go · 20 linesfunc writeConfig(path string, data []byte) error {
f, err := os.CreateTemp(filepath.Dir(path), ".config-*.tmp")
if err != nil {
return err
}
tmp := f.Name()
defer os.Remove(tmp) // no-op once the rename has succeeded
if _, err := f.Write(data); err != nil {
f.Close()
return err
}
if err := f.Sync(); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
return os.Rename(tmp, path)
}go deeper
Be ready to say what a reader sees while a file is being overwritten in place, and to name the four steps: os.CreateTemp beside the target, write, Sync and Close, os.Rename.
Explain why the swap is atomic - a rename rearranges a directory entry rather than moving bytes - and why the temp file has to be created in the target's own directory rather than the system temp directory.
Show where the guarantee stops: rename gives visibility atomicity, not durability and not mutual exclusion between two writers. Say what you would still add for a node that can lose power mid-rewrite.
Frame it as a contract with every consumer of the file: the path is always a complete document. Decide whether that contract belongs in each service or in one shared internal helper, and what you require of readers in return.
## The problem: a rewrite is not one event A file on disk has one name and one set of contents, but changing the contents is many operations. If a process opens the existing target for writing, empties it and then writes the new bytes, the file passes through states that are neither the old document nor the new one: zero length, then a prefix of the new document, then finally the whole thing. Any other process that opens the path during that window reads whatever is there at that instant. That is exactly the shape of a controller that reconciles desired state onto disk by rewriting a small declarative config file that a sidecar process re-reads whenever it notices a change. The sidecar has no idea a rewrite is in progress. Sooner or later it reads half a document, fails to parse it, and either crashes or falls back to defaults. The same window exists for a machine that loses power mid-rewrite: the node comes back with a truncated file and the config is unparseable forever, not just for a moment. ## The fix: publish under a new name, then swap the name The standard answer is that you never modify the target. You build the complete new file somewhere else, make sure it is complete, and then make the target's name point at it in a single step: 1. `os.CreateTemp(dir, pattern)` creates a fresh file with a unique name and returns an open `*os.File`. `dir` must be the target's own directory. 2. Write the entire new contents to it. 3. `f.Sync()` asks the operating system to push the data to stable storage. 4. `f.Close()`, and check the error - a write error can surface here. 5. `os.Rename(f.Name(), target)`. `os.Rename` maps onto the platform's rename primitive. On a POSIX filesystem, renaming onto an existing path replaces the directory entry atomically with respect to other processes: a reader that opens the path either gets the old file or the new file, never a blend and never a missing file. That is the whole guarantee you are buying. ## Why the temp file must live beside the target A rename only rearranges names within one filesystem; it does not move bytes. If the temp file is created in the system temp directory and the target lives on a different mount, the rename fails outright rather than silently copying. Creating the temp file with `filepath.Dir(target)` as its directory keeps both names on the same filesystem, which is the precondition for the atomic swap. ## What happens to readers that already had the file open Replacing a name does not disturb a process that already holds the old file open. On Unix the old contents live on until the last descriptor referring to them is closed; the reader keeps reading the old document to completion. Only the next `os.Open` of that path sees the new file. That is a feature: an in-flight read is never corrupted by your publish. ## Why a unique temp name Using a fixed scratch name like `config.tmp` reintroduces the bug you are fixing. Two rewrites racing each other write into the same scratch file and the loser publishes a mixture; a leftover file or a symlink at that name can also be followed. `os.CreateTemp` substitutes a random string for the `*` in the pattern and creates the file exclusively, so each rewrite has its own scratch name - which you must read back from `f.Name()`, since you did not choose it. ## What the idiom does not buy you It gives you *visibility* atomicity, not durability. A successful `os.Rename` means the name now points at the new file as far as processes are concerned; it does not by itself mean the data and the directory entry have reached the physical disk. Crash-safety needs the file synced before the rename and the parent directory synced after it. Nor does it give you mutual exclusion: two writers can still both publish, last one wins. And the temp file is a real file in the target directory, so a failure path that returns early without removing it leaves litter that a reader globbing the directory may try to parse. The short version an interviewer wants: *write it whole somewhere else, then swap the name in one operation, because a reader must only ever see one complete version of the file.*
- What happens to a process that already had the old file open when os.Rename replaces it?Nothing changes for it. On Unix the open descriptor still refers to the old file, which survives until every descriptor on it is closed, so the reader finishes reading the old contents intact. Only the next open of that path sees the new file. That is why an in-flight reader is never corrupted by a publish.
- Does os.Rename fail if the target path already exists?No - replacing an existing target is the point. On Unix the old directory entry is replaced atomically. On Windows Go's os.Rename also replaces an existing target, but it can fail if another process holds that file open without sharing, so a rewrite loop there has to tolerate and retry the failure.
- Why use os.CreateTemp instead of a fixed scratch name like config.tmp?A fixed name is shared state: two concurrent rewrites clobber each other in it, and a leftover file or symlink at that name may be followed. os.CreateTemp replaces the * in the pattern with a random string and creates the file exclusively, giving each rewrite its own scratch file. Read the actual name back from f.Name().
Instead of repainting the shop sign while customers are looking at it, you paint a whole new sign in the back room and swap the two in one movement.
saying these in an interview costs you the question
- Says overwriting in place is fine because the write is fast
- Assumes one f.Write call lands on disk atomically
- Removes the target first, then writes the replacement
- Creates the temp file in the system temp directory
- Thinks os.Rename copies the bytes to the new path