What does os.WriteFile do to a file that already exists, and when is its perm argument actually used?
answer
- three flags decide everything here
- the file gets shorter before it gets longer
- the mode argument only matters once
- your 0666 is not the 0666 on disk
- an existing 0600 file stays 0600
basics
~20 sos.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.
solid answer
~50 s`os.WriteFile(name string, data []byte, perm os.FileMode) error` opens the target with `O_WRONLY|O_CREATE|O_TRUNC`, writes the bytes, and closes it. Two consequences trip people up. First, it is a **replace**, not an append: an existing file is truncated to zero and the old contents are gone the moment the call starts. Second, `perm` is a **create-only** argument — it is the mode used when the file has to be created, and it is masked by the process umask, so passing `0o666` on a typical Linux box yields `0644` on disk. If the file already exists, `perm` is ignored entirely: a file that is `0600` stays `0600` no matter what you pass. If you need a specific mode on an existing file you have to set it explicitly with `os.Chmod`. And because truncate-then-write is several steps, the file on disk is genuinely short or empty in between.
code
go · 11 lines// gen/model.go already exists on disk with mode 0600.
out := []byte("package gen\n\nvar Version = 2\n")
if err := os.WriteFile("gen/model.go", out, 0o644); err != nil {
return fmt.Errorf("write generated file: %w", err)
}
// Contents: replaced. Mode: still 0600, because perm applies only on create.
if err := os.Chmod("gen/model.go", 0o644); err != nil {
return err
}go deeper
Remember the headline: it replaces, it does not append, and it creates the file if it is missing. Know that the third argument is a file mode written in octal.
Explain the O_WRONLY|O_CREATE|O_TRUNC combination and derive the behaviour from it, including why perm is create-only and how umask changes the mode that lands on disk.
Show that you see the missing guarantee: truncate-then-write leaves a window and destroys the old contents up front, so this is the wrong primitive for a file other readers touch or for data you cannot afford to lose on a failed write.
Own the convention: which files in the system may be rewritten in place at all, how modes on generated artefacts are set deliberately rather than inherited by accident, and what the team's default is when a write can fail midway.
## The call ```go func os.WriteFile(name string, data []byte, perm os.FileMode) error ``` It is the mirror of `os.ReadFile`: hand it a path and a byte slice, and it puts those bytes in that file. There is no returned handle, nothing to close, and no partial-write bookkeeping — either the whole slice went out or you get an error. ## What it actually does Under the hood it is roughly: 1. `OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm)` 2. `Write(data)` 3. `Close()` Every surprising behaviour follows from those flags. **`O_CREATE`** means the file is created if it does not exist. **`O_TRUNC`** means that if it *does* exist, it is truncated to zero length first. **`O_WRONLY`** means it is opened write-only, so nothing is read back. There is no `O_APPEND` anywhere: `os.WriteFile` never appends. If you want to add to the end of a file you need `os.OpenFile` with `O_APPEND` instead, which is a different call with a different shape. ## The perm argument is create-only This is the part that most often produces a bug report. `perm` is the third argument to `OpenFile`, and the operating system uses the mode argument of `open(2)` **only when the call actually creates the file**. So: - File does not exist: it is created, with mode `perm &^ umask`. - File exists: the mode argument is ignored. The file keeps whatever mode it already had. A generated file that someone once created as `0600` will stay `0600` through a thousand `os.WriteFile(path, out, 0o644)` calls. Nothing errors; the mode simply does not change. If you want the mode to be `0644` for certain, you must call `os.Chmod` explicitly after the write, or delete and recreate the file. ## The umask filter On Unix-like systems each process carries a **umask**, a set of permission bits that are stripped from every file it creates. The common default is `022`, which clears group-write and other-write. So the idiomatic `0o666` passed to `os.WriteFile` becomes `0644` on disk; `0o777` on a directory becomes `0755`. Go does not apply the umask itself — the kernel does, at creation — which is why you cannot see it in the Go source and why the effective mode differs between machines with different umasks. Writing `0o644` and expecting exactly `0644` happens to work under umask `022`, but only because the bits you asked for are already a subset of what the umask allows. Write the mode in octal and say so: Go 1.13 added the `0o644` literal form, which is clearer than `0644` and much clearer than `420`. ## Truncate first, write second The order matters more than the API suggests. Between the truncation and the last byte of the write, the file on disk is empty or holds only a prefix of the new data. `os.WriteFile` gives you no atomicity: it is not a swap, it is an in-place rewrite. Anything else reading that path during the call — another process, another goroutine, a file watcher, an editor — can observe the intermediate state. Worse, if the write fails halfway (disk full, a signal, the process dying), you are left with a partial file and the old contents are unrecoverable, because the truncation already happened. So the honest summary is: `os.WriteFile` is the right tool when you own the file and nothing else looks at it while you are writing, and the wrong tool when other readers can arrive at any moment. For files other processes read concurrently you want the new contents to become visible as a complete unit rather than to overwrite in place — a different pattern with its own mechanics. ## Errors A non-nil error can come from the open (path missing a parent directory, permission denied, the target is a directory) or from the write (out of space, I/O error). `os.WriteFile` also reports a close error if the write itself succeeded, so a returned nil error means the descriptor was closed cleanly too — which matters on filesystems that surface write errors late. ## History Before Go 1.16 this was `ioutil.WriteFile`, same signature, same semantics. Go 1.16 moved it to `os` and deprecated `io/ioutil`. ## What an interviewer is checking That you know it truncates rather than appends; that you can state the create-only rule for `perm` and the umask filter without hedging; and that you notice the absence of any atomicity guarantee before someone hands you an incident caused by it.
- You pass 0o644 but the file on disk stays 0600. Why, and how do you actually change it?Because the file already existed, so `os.WriteFile` did not create it and the mode argument was ignored — the kernel only honours it on creation. To change the mode you call `os.Chmod` explicitly after writing, or remove the file first so the next write creates it fresh.
- Why does os.WriteFile(path, data, 0o666) commonly produce a 0644 file on Linux?The process umask, typically `022`, strips group-write and other-write from the mode at creation time. The kernel applies it, not Go, so the effective mode is `perm &^ umask` and varies with the environment the program runs in.
- Can you use os.WriteFile to append a line to a log file?No. It opens with O_TRUNC and no O_APPEND, so it replaces the contents rather than adding to them. Appending needs `os.OpenFile` with the `O_APPEND` flag and a normal `Write` on the returned file.
- If the write fails halfway through, what is left on disk?A truncated file holding whatever prefix made it out. The old contents are already gone, because truncation happens when the file is opened, before any of the new data is written. There is no rollback: the error tells you the write failed, not that the file is intact.
saying these in an interview costs you the question
- Thinks os.WriteFile appends to an existing file
- Expects perm to fix the mode of an existing file
- Assumes the replace is atomic and readers never see a partial file
- Ignores umask and insists 0666 means 0666 on disk
- Believes a failed write leaves the previous contents intact
- Says os.WriteFile returns the number of bytes written