skip to content

Files and Directories

os.OpenFile flags and permission bits, os.ReadFile and os.WriteFile, walking with fs.DirEntry, durable writes. Asked because os.WriteFile truncates and a write without Sync can vanish in a crash.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

20

Why write a config file to a temp file and os.Rename it onto the target instead of overwriting in place?

level: juniorimportance: must knowfreq 48%

answer

  1. two states only, never a half state
  2. a reader may open it at any instant
  3. build the new one under another name
  4. swapping a name is one operation
  5. os.CreateTemp beside the target, then os.Rename

basics

~20 s

Overwriting 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 s

Rewriting 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 lines
go
func 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

What does os.ReadFile give you, and what work does it do that you would otherwise write by hand?

level: juniorimportance: must knowfreq 80%

basics

~20 s

os.ReadFile opens the named file, reads it to the end, closes it, and returns the whole contents as a []byte together with an error. Reaching end of file is not an error, so a successful read returns a nil error.

open as a page

What does Go's os.Stat return, and what can you read from the fs.FileInfo it gives you?

level: juniorimportance: must knowfreq 62%

basics

~20 s

os.Stat returns an fs.FileInfo for a named path, plus an error. FileInfo is an interface: Name, Size, Mode, ModTime, IsDir and Sys report the base name, byte size, mode bits, modification time and whether the name is a directory.

open as a page

In Go, what is the difference between os.Open, os.Create and os.OpenFile?

level: juniorimportance: must knowfreq 70%

basics

~10 s

os.Open opens an existing file read-only. os.Create creates or truncates a file for read-write with mode 0666. os.OpenFile is the general form behind both: you pass the open flags and the permission bits yourself.

open as a page

What does os.ReadDir return, and how do you tell whether an entry is a subdirectory?

level: juniorimportance: must knowfreq 68%

basics

~10 s

os.ReadDir returns a sorted slice of fs.DirEntry values for one directory plus an error. Each entry gives Name, IsDir and Type; IsDir reports a subdirectory. It reads a single level and never recurses.

open as a page

What does os.WriteFile do to a file that already exists, and when is its perm argument actually used?

level: middleimportance: must knowfreq 62%

basics

~20 s

os.WriteFile truncates an existing file to zero and writes the new bytes over it, creating the file only when it is missing. The perm argument applies at creation only, is filtered by umask, and never changes an existing file's mode.

open as a page

When does os.Rename fail with a cross-device link error, and where must the temp file be created?

level: middleimportance: should knowfreq 40%

basics

~20 s

os.Rename only rearranges names inside one filesystem. If the temp file was created in the system temp directory and the target sits on another mount, the call fails with an *os.LinkError wrapping EXDEV. Create the temp file in the target's own directory.

open as a page

If a rewrite fails after os.CreateTemp, what is left in the directory and how should the code clean it up?

level: middleimportance: should knowfreq 32%

basics

~20 s

A partially written temporary file is left beside the target, where it had to be created. Register defer os.Remove(f.Name()) right after os.CreateTemp: it clears the leftover on every failure path and is a harmless no-op once the rename has succeeded.

open as a page

How do os.CreateTemp and os.MkdirTemp name what they create, and who is responsible for deleting it?

level: middleimportance: should knowfreq 44%

basics

~20 s

Both take a directory and a pattern; a random string replaces the last asterisk in the pattern, or is appended when there is none. An empty directory argument means os.TempDir. Neither cleans up: the caller must remove the file or directory itself.

open as a page

How do you test an fs.FileMode in Go for a directory, a symlink and its permission bits?

level: middleimportance: should knowfreq 38%

basics

~20 s

fs.FileMode packs type bits in the high bits and nine permission bits in the low bits. Use m.IsDir(), test a link with m&fs.ModeSymlink != 0, and take permissions with m.Perm(). Never compare the whole mode with ==.

open as a page

In Go, how do os.Stat and os.Lstat differ when the name is a symbolic link?

level: middleimportance: should knowfreq 50%

basics

~20 s

os.Stat follows a symbolic link and describes its target, failing if that target is gone. os.Lstat describes the link itself: the Mode it returns has the fs.ModeSymlink bit set, and os.Readlink then gives the raw target path stored in the link.

open as a page

Which os.OpenFile flags make a daemon's lockfile creation fail if the file already exists?

level: middleimportance: should knowfreq 45%

basics

~10 s

os.O_CREATE combined with os.O_EXCL, plus a write access mode. The operating system creates the file only if it is absent and otherwise fails, in one indivisible step. The loser's error satisfies errors.Is(err, fs.ErrExist).

open as a page

Why does os.OpenFile with perm 0666 often create a file with mode 0644?

level: middleimportance: should knowfreq 50%

basics

~20 s

The perm argument is only a request. The kernel clears the bits set in the process umask, so with the usual umask of 022 a requested 0666 becomes 0644. Go neither reads nor applies the umask itself.

open as a page

Why is filepath.WalkDir cheaper than filepath.Walk over a large directory tree?

level: middleimportance: should knowfreq 52%

basics

~10 s

filepath.WalkDir hands its callback an fs.DirEntry that the directory read already produced, so it needs no per-entry metadata call. filepath.Walk hands an fs.FileInfo, which forces a stat of every file and directory it visits.

open as a page

os.WriteFile rewrites a file in place, so how can a concurrent os.ReadFile of it return half a document and a nil error?

level: seniorimportance: should knowfreq 33%

basics

~20 s

os.WriteFile truncates the file to zero before writing, so for a moment it really is empty or holds only a prefix. A reader that opens then reaches a genuine end of file, gets fewer bytes, and sees no error.

open as a page

Your indexer's filepath.WalkDir stops at one unreadable directory and writes a partial manifest — how should the callback handle that error?

level: seniorimportance: should knowfreq 46%

basics

~20 s

filepath.WalkDir calls the callback a second time for that directory with err set to the read failure. Returning that error stops the whole walk; record it and return nil to keep walking, then refuse to publish the manifest as complete.

open as a page

What do fs.SkipDir and fs.SkipAll do when returned from a filepath.WalkDir callback?

level: middleimportance: nice to knowfreq 36%

basics

~10 s

Returning fs.SkipDir skips the current directory's contents, or the rest of the containing directory when the entry is a file. fs.SkipAll ends the whole walk immediately, and filepath.WalkDir still returns nil.

open as a page

After os.Rename returns nil, what still has to be synced for the new file to survive a power loss?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

Two syncs: the temp file's data with f.Sync before the rename, and the parent directory after it, by opening that directory and calling Sync. A successful rename swaps the name in cache only; neither the data nor the new entry is guaranteed on disk.

open as a page

How does os.SameFile let a backup scanner notice it already archived a file under another name?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

os.SameFile compares two FileInfo values by the file's underlying identity — device and inode on Unix — rather than by path, size or timestamp. A scanner keeps the FileInfos it has archived and skips any new one that SameFile matches.

open as a page

What does os.O_APPEND guarantee when several writers share one journal file?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

Each write lands at the current end of the file as one operation, so concurrent writers never overwrite each other. It does not make a buffered or multi-call record atomic, and says nothing about ordering or durability.

open as a page