skip to content

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

level: middleimportance: should knowfreq 45%

answer

  1. two flags, and one is useless alone
  2. create-or-fail as a single step
  3. the loser gets an existing-file error
  4. os.O_CREATE by itself will happily reuse the file

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).

solid answer

~50 s

Open it with `os.O_CREATE|os.O_EXCL|os.O_WRONLY`. `O_EXCL` is only meaningful next to `O_CREATE`, and together they tell the kernel to create the file *or* fail — the existence test and the creation happen as a single operation, so two daemon instances racing to start cannot both believe they won. The loser gets a `*fs.PathError` that you match with `errors.Is(err, fs.ErrExist)`, never by inspecting the message text. What this does not give you is a lock: the file outlives the process, so a daemon killed with SIGKILL leaves the lockfile behind and the next start refuses to run. That is why a real implementation writes its pid into the file, removes it on clean shutdown, and has a documented story for a stale one. A separate existence check followed by `os.Create` is not equivalent — it reintroduces the window that `O_EXCL` exists to close, and `os.Create` would also truncate the current holder's file.

code

go · 10 lines
go
f, err := os.OpenFile("/var/run/journald.lock",
	os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
if errors.Is(err, fs.ErrExist) {
	return fmt.Errorf("another instance already holds %s", "/var/run/journald.lock")
}
if err != nil {
	return err
}
fmt.Fprintf(f, "%d\n", os.Getpid()) // so a later start can judge a stale lock
defer os.Remove(f.Name())

go deeper

for a junior

Recall the pair: os.O_CREATE together with os.O_EXCL means create it or fail. Know that plain os.Create would happily open the existing file instead and empty it.

for a middle

Explain that the kernel does the existence test and the creation as one operation, and show the errors.Is check against fs.ErrExist rather than reading the error text.

for a senior

Bring the operational half: a lockfile survives the process, so describe the stale-lock story you would ship — pid in the file, removal on clean exit, and an explicit recovery path for a killed daemon.

for a principal

Own whether single-instance enforcement belongs in the program at all, versus the service manager or the orchestrator that already guarantees one replica, and what the failure mode of each choice costs during an incident.

### The flags ```go f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if errors.Is(err, fs.ErrExist) { return fmt.Errorf("another instance is already running") } if err != nil { return err } ``` `os.O_CREATE` says "make the file if it is not there". On its own it is happy to reuse an existing file. `os.O_EXCL` changes that pairing into "make the file, and fail if it is already there". `O_EXCL` is defined only in combination with `O_CREATE`; on its own it does nothing useful, and code that passes it alone is confused about what it does. The value of the pair is that the operating system performs the existence test and the creation as one indivisible operation. Two processes that reach that line at the same instant do not both succeed. Exactly one gets a handle; the other gets an error. Any Go code that instead asks whether the path exists and then creates it has separated those two steps, and something else can create the file in the gap. ### Recognising the loser's error `os.OpenFile` returns `*fs.PathError`, a struct with `Op`, `Path` and a wrapped `Err`. The wrapped value for this case is the platform's file-exists error, which the standard library maps to the sentinel `fs.ErrExist` (also reachable as `os.ErrExist`, the same value). Because the sentinel is wrapped rather than returned directly, `err == fs.ErrExist` is false and `errors.Is(err, fs.ErrExist)` is true. Matching on `err.Error()` containing the word "exists" is wrong for the same reasons it is always wrong: the text is platform-specific and not part of any contract. ### What a lockfile actually guarantees This is where candidates who have only read about the flags separate from candidates who have operated a daemon. Exclusive creation gives you mutual exclusion **over the existence of a name**, not over a running process. Concretely: - The file has no association with the process that made it. Nothing removes it when that process dies. - A clean shutdown can delete it with `os.Remove`, but a `SIGKILL`, an OOM kill, or a power loss cannot run that code. - After such a death, every subsequent start finds the file and refuses to run, which is an outage that looks like a bug in the daemon rather than in the lock. The usual mitigations are to write the pid (and often the boot id or start time) into the file after creating it, so a later start can decide whether the recorded process is still alive, and to document a manual override. The alternative is a real advisory lock from the operating system, which the kernel releases when the descriptor closes for any reason including a crash — but Go's standard library exposes no portable API for that, so it means platform-specific code. One more caveat worth knowing: the atomicity of exclusive create is a property of the filesystem, and it has historically not been reliable over some network filesystems. A lockfile on a shared network mount is a weaker guarantee than the same code on a local disk. ### Why not os.Create `os.Create` is `O_RDWR|O_CREATE|O_TRUNC`. Used as a lock it fails twice over. It succeeds when the file already exists, so both instances think they hold the lock, and its `O_TRUNC` empties the file — destroying the pid the current holder wrote there. The whole point of the exclusive flag is that the call is allowed to fail, and a candidate who reaches for a call that cannot fail has not understood the requirement. ### The same pattern beyond lockfiles Exclusive create is the general Go idiom for "claim a name": one worker in a pool claiming a unit of work by creating a marker file, a first-run initialisation that must happen once, a cache entry that must not be written twice. In each case the semantics you are buying are the same — the create either wins or reports `fs.ErrExist` — and the same caveats about crash recovery apply. Whenever the claim must be released automatically on process death, a file whose only property is existence is the wrong primitive.

  • What error does the losing process get, and how should the code recognise it?
    A `*fs.PathError` whose wrapped error is the platform's file-exists error, mapped to the sentinel `fs.ErrExist`. Test it with `errors.Is(err, fs.ErrExist)`. A direct `==` comparison fails because the sentinel is wrapped, and matching the message string is platform-dependent and unsupported.
  • What happens to the lockfile if the daemon is killed with SIGKILL?
    It stays on disk. The file has no link to the process, so nothing removes it, and every later start then fails as though another instance were running. Real implementations write the pid into the file so a start can decide whether the recorded owner is still alive, and document a manual clear.
  • Does os.O_EXCL do anything without os.O_CREATE?
    Nothing you should rely on. The flag is defined as a modifier of the create behaviour, and passing it alone is a sign the author expected it to mean exclusive access to the file, which it does not — it never keeps other processes from opening a file that already exists.

It is the difference between asking the front desk for room 12 if it is free and asking whether room 12 is free before walking up to it. Only the first question and answer happen without anyone else slipping in between.

saying these in an interview costs you the question

  • Calls os.Stat first and then creates, leaving a window
  • Uses os.Create for a lockfile and truncates the holder's pid
  • Matches the failure with strings.Contains on the message
  • Thinks the lockfile disappears when the process is killed
  • Believes os.O_EXCL alone excludes other openers