skip to content

Program Inputs at Startup

Where a program's settings come from before it does any work: the flag package, os.Args, os.Getenv and os.LookupEnv, and the working directory. Interviewers ask how the sources are ordered.

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

explore

questions

13

In Go, how do you let an environment variable set a flag's default so an explicit flag still wins?

level: juniorimportance: must knowfreq 60%

answer

  1. the flag package never reads the environment
  2. a default is just an argument
  3. lookup happens before Parse, not after
  4. seed the default, let Parse override it
  5. LookupEnv's second result means present

basics

~20 s

Read the variable with os.LookupEnv before flag.Parse and pass its value as the default argument when you declare the flag. flag.Parse then overwrites that default only for flags that actually appeared on the command line.

solid answer

~50 s

Go's `flag` package knows nothing about the environment, so the precedence order is code you write. The lever is that a flag's default is an ordinary argument: resolve it first, declare the flag with it, then call `flag.Parse`. For each setting I start from a compiled-in default, overwrite it when `os.LookupEnv` reports the variable is present, parse it into the right type with `time.ParseDuration` or `strconv.Atoi`, and pass the result as the default to `flag.Duration` or `flag.IntVar`. `flag.Parse` assigns only for flags that actually appeared on the command line, so the resulting order is flag beats environment beats built-in default, with no extra bookkeeping. Two details matter: do the lookup before `Parse`, never after, or you clobber what the operator typed; and decide explicitly what a present-but-empty variable means, because `os.LookupEnv` reports it as set.

code

go · 12 lines
go
// order: built-in default < environment variable < command-line flag
func durationFlag(name, env string, def time.Duration, usage string) (*time.Duration, error) {
	if raw, ok := os.LookupEnv(env); ok {
		d, err := time.ParseDuration(raw)
		if err != nil {
			return nil, fmt.Errorf("%s=%q: %w", env, raw, err)
		}
		def = d
	}
	// flag.Parse will overwrite this only if -name is on the command line
	return flag.Duration(name, def, usage), nil
}

go deeper

for a junior

Be ready to write the four lines from memory: look the variable up, convert it, pass it as the flag's default, then call flag.Parse once after all flags are declared.

for a middle

Explain why seeding the default gives you precedence for free, and what flag.Parse actually does when a flag is absent from the command line. Know that a present-but-empty variable still reports as set.

for a senior

Show how you keep this honest in production: one resolution function, a table-driven test over the precedence cases, and a startup log of the effective values with secrets redacted so a new environment can be debugged in one look.

for a principal

Own the convention rather than the code. Pick one resolution order, implement it once for every service, and require that the effective config is visible at startup; fleet-wide consistency matters more than which order you choose.

## The problem A service usually needs the same setting to be reachable three ways: a value compiled into the binary so a developer can just run it, an environment variable so a deployment platform can inject it, and a command-line flag so a human can override it for one run. The conventional resolution order is **flag beats environment beats built-in default** — most specific and most deliberate wins. Go's standard library gives you no config framework for this. The `flag` package parses the command line and nothing else; `os.LookupEnv` reads one environment variable and nothing else. The layering is a dozen lines you write in `main`. ## The mechanism Every flag constructor takes the default as an argument: ```go flag.Duration(name string, value time.Duration, usage string) *time.Duration flag.DurationVar(p *time.Duration, name string, value time.Duration, usage string) ``` That `value` argument is evaluated by ordinary Go code before `flag.Parse` ever runs. So instead of a constant, pass the environment-resolved value: 1. Start with the built-in default. 2. Call `os.LookupEnv("MYSVC_REQUEST_TIMEOUT")`. Its second result is a boolean reporting whether the variable is **present**, which is the whole reason to prefer it over `os.Getenv` here — you need present-with-empty-value to be distinguishable from absent. 3. If present, convert the string to the field's real type: `time.ParseDuration` for durations, `strconv.Atoi` or `strconv.ParseInt` for integers, `strconv.ParseFloat` for percentages, `strconv.ParseBool` for switches. Check the error; a bad value is a startup failure, not something to shrug off. 4. Declare the flag with that resolved value as its default. 5. Call `flag.Parse` once, after every flag is declared. `flag.Parse` walks the command-line arguments and calls `Set` only for flags that actually appear there. A flag the operator did not type keeps whatever default you seeded. That is the entire precedence engine. ## Why not read the environment after Parse The tempting shortcut is to call `flag.Parse` first and then apply the environment on top: ```go flag.Parse() if v, ok := os.LookupEnv("MYSVC_ADDR"); ok { addr = v // wrong: this beats the flag the operator typed } ``` Now the environment always wins and the flag is decorative — precisely backwards from what operators expect, and maddening to debug at 3am when `-addr` visibly does nothing. You could repair it by asking the `flag` package which flags were explicitly set, but seeding the default keeps a single resolution path and needs no such check. ## The empty-string decision `os.LookupEnv` returns `("", true)` for `MYSVC_ADDR=`. Deployment systems produce that constantly: a template renders an unset value, a shell exports a variable it never filled in. You must pick a rule and document it. For optional strings, treating empty as "not set" (fall through to the next layer) is usually kindest. For a typed setting, empty is a malformed value and should fail startup — an empty string is not a duration, and silently substituting zero is how a service ends up with a timeout of `0`. ## Naming and discoverability Prefix the variables with the service name (`FEATUREFLAGS_REQUEST_TIMEOUT`, not `TIMEOUT`) so they cannot collide with something the platform already sets, and derive the name mechanically from the flag name so a reader can guess one from the other. Mention the variable in the flag's usage string; `flag` prints it in `-h` output, and that is the only documentation some operators will find. ## Proving it works Precedence is exactly the kind of logic that looks obviously correct and is quietly wrong, so it is worth a table-driven test: rows for default-only, environment-only, flag-only, and environment-plus-flag, each asserting the resolved configuration struct. Write the resolution as a function taking the argument slice and an environment lookup rather than reading globals, and the test needs no process-level trickery. Finally, log the resolved configuration once at startup with secrets redacted. The engineer bringing the service up in a new environment for the first time then sees which value each setting ended up with, instead of guessing which of the three layers won.

  • Why must the environment lookup happen before flag.Parse rather than after it?
    After `Parse` the variable already holds either the operator's value or the compiled default, and nothing distinguishes them without extra bookkeeping. Applying the environment there overwrites an explicit flag, inverting the expected precedence. Seeding the default first keeps one resolution path: `Parse` assigns only for flags actually present on the command line.
  • The deployment sets the variable to an empty string. Does that count as set?
    `os.LookupEnv` says yes — it returns `("", true)` — so you must decide. For an optional string, treating empty as unset and falling through to the next layer is usually right. For a typed setting it should fail startup, since an empty string is not a valid duration or integer and quietly turning it into zero is a real outage.
  • How would you test that the precedence is actually flag over environment over default?
    Make resolution a function that takes the argument slice and an environment lookup instead of reading globals, then write a table-driven test with one row per case: nothing set, environment only, flag only, and both set with different values. Each row asserts the whole resolved config struct, so a future reordering of the layers fails loudly.

The environment fills the form in pencil before anyone looks at it; the command line writes over it in ink.

saying these in an interview costs you the question

  • Applies the environment variable after flag.Parse, so the flag never wins
  • Believes the flag package reads environment variables by itself
  • Uses os.Getenv and cannot tell an absent variable from an empty one
  • Declares flags after calling flag.Parse and wonders why nothing is set
open as a page

In Go, what is the difference between os.Getenv and os.LookupEnv?

level: juniorimportance: must knowfreq 70%

basics

~10 s

os.Getenv returns only the value, so an unset variable and one set to the empty string both come back as "". os.LookupEnv returns the value plus a presence boolean, keeping those two cases distinct.

open as a page

In Go's flag package, why does flag.String return a *string, and when may you read it?

level: juniorimportance: must knowfreq 70%

basics

~20 s

flag.String registers a flag before the command line has been read, so it can only hand back a pointer to storage it will fill in later. Call flag.Parse() first, then read the value as *ptr.

open as a page

How do you implement git-style subcommands in Go with flag.NewFlagSet, each owning its own flags?

level: middleimportance: must knowfreq 55%

basics

~10 s

Give each subcommand its own *flag.FlagSet from flag.NewFlagSet, switch on os.Args[1] to choose one, and call that set's Parse on os.Args[2:]. Each set owns its own flag names, usage text and operands.

open as a page

Why is calling os.Chdir inside a Go library or long-running server risky?

level: middleimportance: should knowfreq 45%

basics

~10 s

The working directory is one attribute of the whole process, not something each goroutine owns. os.Chdir changes how every relative path in the program resolves, including paths other goroutines are opening at that instant.

open as a page

Why does Go's flag package ignore -v in `tool build main.go -v`, and what does `--` do?

level: middleimportance: should knowfreq 50%

basics

~20 s

Go's flag package stops parsing at the first argument that is not a flag, so -v after main.go is treated as a positional operand and never assigned. A bare -- also ends parsing, and everything after it becomes an operand.

open as a page

A Go service starts cleanly but every request fails instantly with a deadline error; the timeout comes from an env var. How do you diagnose and fix it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

The timeout almost certainly resolved to a zero time.Duration, because the variable was absent or malformed and the parse error was discarded. context.WithTimeout with zero builds an already-expired deadline. Fix it by checking the parse error and validating the value before the server starts.

open as a page

A Go package's tests pass individually under go test -run but fail when the whole package runs. How do you diagnose it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Treat it as order dependence, not flakiness: one test leaves process-global state behind, usually an environment variable or the working directory. Reproduce with go test -count=2 and -shuffle=on, then undo per test with t.Setenv and t.Chdir.

open as a page

In a Go CLI with per-subcommand flag sets, why is a -verbose flag silently false, and how do you catch it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Because nothing parsed the set it was registered on. A flag defined on one flag set and never reached by that set's Parse keeps its default, with no error anywhere, and the default is indistinguishable from a deliberate choice.

open as a page

Across a fleet of Go services wired with flags and env vars, which source wins, and when must startup fail?

level: principalimportance: should knowfreq 38%

basics

~20 s

Pick one order for every service, usually flag over environment over built-in default, and implement it once. Then classify settings: anything unsafe to guess must abort startup with a nonzero exit, while settings with a documented safe default may boot and log what they used.

open as a page

After flag.Parse, how can a Go program tell which flags were actually set on the command line?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Call flag.Visit after flag.Parse: it invokes your function only for flags that were set on the command line. flag.VisitAll walks every declared flag, set or not, which is what you want for dumping the effective configuration.

open as a page

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%

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.

open as a page

You own a Go CLI other teams call from CI. How do you decide what becomes a subcommand, a flag, or a positional?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Treat the command line as a public API. Subcommands for distinct verbs, flags for anything optional or likely to change, positionals only for the one obvious operand — Go's flag package offers no alias or deprecation mechanism once a name ships.

open as a page