skip to content

Why does os.Open("config.yaml") work in local development but fail when the same Go binary runs as a service?

level: middleimportance: nice to knowfreq 28%

answer

  1. who chose the directory you started in?
  2. your shell, or the service supervisor
  3. the binary's location is a different question
  4. os.Getwd answers one, os.Executable the other

basics

~20 s

A relative filename resolves against the process's working directory, inherited from whatever started the program: your shell in the source folder locally, often / under a service manager. The binary's own location is separate, reported by os.Executable.

solid answer

~50 s

Relative paths are resolved by the operating system against the process's current working directory, and that directory is inherited from the parent. Locally you run the program from the directory that happens to contain `config.yaml`, so it is found; deployed, the supervisor starts the binary from `/` or some unit-specific directory and the same call fails with a not-exist error. `os.Getwd` tells you what that directory currently is. It is unrelated to where the binary lives — that is `os.Executable`, which returns the path of the running executable and is the usual basis for "look next to the binary". Even so, `os.Executable` is only a hint: it may return a symlink or its target, and nothing promises the file is still there. The robust answers are to embed the file into the binary, or to take an explicit path from configuration.

code

go · 6 lines
go
exe, err := os.Executable()
if err != nil {
	return err
}
cfg := filepath.Join(filepath.Dir(exe), "config.yaml")
f, err := os.Open(cfg)

go deeper

for a junior

Recall that a relative filename is resolved against the directory the process was started in, and that os.Getwd is how you ask what that directory is.

for a middle

Explain the split: the inherited working directory versus os.Executable's report of the binary's own path, and why the second is only a hint when symlinks or redeploys are involved.

for a senior

Show how you take the ambiguity out of a deployed service — embed static assets or require an absolute configured path, so a failure is a clear configuration error rather than a directory-dependent mystery.

for a principal

Own the convention across services: whether assets ship inside the binary or beside it, who sets the working directory in the deployment description, and how that is kept uniform so on-call engineers can predict what a process will read.

Two different directories get confused here, and separating them answers the question. ## 1. The current working directory Every process has one, and it is **inherited from the process that started it**. When you run a program from a terminal, the shell's directory becomes the program's directory. When a supervisor, a container runtime, a scheduler or an IDE starts it, that parent decides — often `/`, often a configured value, rarely the directory you had in mind. The operating system resolves every *relative* path — `"config.yaml"`, `"./data"`, `"../shared"` — against it, at the moment of the syscall. `os.Getwd() (string, error)` reports it. Note the error: the directory the process is sitting in can be deleted while the process runs, and then even asking becomes an error. So `os.Open("config.yaml")` is not really asking for a file; it is asking for "config.yaml, relative to wherever we happen to have been started". During development you are almost always standing in the project directory when you run it, and that is why it works. In production nothing puts you there. ## 2. Where the binary lives `os.Executable() (string, error)` returns the path of the executable that started the current process. This is what people reach for when they want "the file that shipped next to my binary": exe, err := os.Executable() if err != nil { return err } cfg := filepath.Join(filepath.Dir(exe), "config.yaml") That is legitimate, but it comes with documented caveats worth stating in an interview: - If the process was started through a **symlink**, the result may be the symlink or the path it points to, depending on the operating system. - There is **no guarantee the path still points at the right executable**. On Unix the file can be renamed, replaced by a deploy, or deleted while the process keeps running. - The result is not a promise about *anything else* being next to it. `go install` puts the binary in a bin directory on its own; a minimal container image copies the binary and nothing more. "Next to the binary" is a deployment convention, not a language guarantee. `os.Args[0]` is not a substitute either: it is whatever the parent chose to pass as the program name, and a parent may pass anything at all. ## 3. What to do about it Ranked roughly by robustness: 1. **Embed the file** into the binary with the `//go:embed` directive when it is genuinely static — templates, migrations, a default configuration. Then there is no path and no failure mode. 2. **Take the path explicitly** from configuration, and make it absolute. The program stops guessing and the operator can see exactly what it will read. 3. **Set the working directory deliberately** in the deployment description if relative paths must keep working, and say so in the program's documentation. 4. **Derive it from `os.Executable`** when the asset really does ship alongside the binary, accepting the caveats above. What you should not do is call `os.Chdir` at some later point to "fix" it: that changes the directory for the whole process, including every other goroutine. ## The interview signal The crisp version of the answer is one sentence: *relative paths resolve against the directory the process was started in, not against the source tree and not against the binary's location.* Everything else — `os.Getwd`, `os.Executable`, embedding — is how you make the program stop caring.

  • What are the documented caveats of os.Executable?
    If the process was started through a symlink the result may be the link or its target, depending on the operating system, and nothing guarantees the path still points at the correct executable — the file can be replaced or deleted by a deploy while the process runs. It is a hint about where you came from, not a durable handle.
  • Why is os.Args[0] not a reliable way to find the binary?
    `os.Args[0]` is simply the argument the parent process chose to pass as the program name. A shell usually passes something sensible, but a parent may pass a bare name, a relative path, or an entirely made-up string. `os.Executable` asks the operating system instead of trusting an argument.
  • How would you make the program not depend on either directory?
    Embed genuinely static files into the binary with the `//go:embed` directive so there is no path to get wrong, or require an explicit absolute path from configuration so the operator can see what will be read. If relative paths must work, set the working directory deliberately in the deployment description.

saying these in an interview costs you the question

  • Thinks a relative path resolves against the executable's directory
  • Assumes os.Getwd and os.Executable return the same thing
  • Believes os.Args[0] reliably gives the binary's path
  • Says the working directory is fixed when the program is compiled
  • Fixes it by calling os.Chdir later in the program