How to Build a CLI with Go and Cobra Flags
Build a Go CLI with Cobra flags so persistent flags apply to all subcommands and local flags are scoped to one command.
On this page
Introduction
The hardest part of scope CLI flags is not writing the code — it is deciding what the code is allowed to reject. A Cobra flags setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in Cobra flags, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.
The cost of scope CLI flags without Cobra flags is paid in incidents, not in code. The code looks fine because the happy path works, and the happy path is what you demo. The failure path is what you operate, and the failure path is where Cobra flags earns its keep. Instrument the rejections, alert on the spikes, and let the metric tell you when the guard is misaligned with reality.
The sections below walk through the concrete decisions: where to put the cobra flags check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.
Persistent flags
Persistent flags is the first decision because it determines what the rest of the Cobra flags pipeline can assume. A wrong choice here turns into scope CLI flags under load, and the invariant you thought Cobra flags guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Persistent flags is the one most teams skip: drive cobra flags to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
root.PersistentFlags().StringP("config", "c", "", "config file")
build.Flags().BoolP("watch", "w", false, "watch mode")
The go above is the shape to ship for scope CLI flags. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.
Local flags
The mechanism behind Local flags is straightforward in Cobra flags, but the consequences ripple. A wrong choice here turns into scope CLI flags under load, and the invariant you thought Cobra flags guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Local flags is the one most teams skip: drive cobra flags to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Flag precedence
Flag precedence is where most Cobra flags implementations leak performance. A wrong choice here turns into scope CLI flags under load, and the invariant you thought Cobra flags guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Flag precedence is the one most teams skip: drive cobra flags to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
func TestPersistentFlag(t *testing.T) {
root.SetArgs([]string{"--config", "x", "build"})
root.Execute()
assert.Equal(t, "x", cfgFile)
}
Pin the behavior with this test. A Cobra flags setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.
Default values
The hard part of Default values is not the happy path — it is what happens when cobra flags fails halfway. A wrong choice here turns into scope CLI flags under load, and the invariant you thought Cobra flags guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Default values is the one most teams skip: drive cobra flags to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Required flags
Required flags should be observable without a debugger. A wrong choice here turns into scope CLI flags under load, and the invariant you thought Cobra flags guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Required flags is the one most teams skip: drive cobra flags to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Test flag parsing
Test flag parsing is a trade, not a winner. A wrong choice here turns into scope CLI flags under load, and the invariant you thought Cobra flags guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test flag parsing is the one most teams skip: drive cobra flags to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Common pitfalls
The common pitfalls in scope CLI flags all reduce to one mistake: treating Cobra flags as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.
- Skipping the cobra flags boundary so scope CLI flags enters unvalidated.
- Assuming Cobra flags is fast without measuring the tail latency under load.
- Letting scope CLI flags fail silently instead of failing loudly with a metric.
- Testing only the happy path so the cobra flags failure surfaces in production.
- Sharing mutable state across requests so one bad input poisons the next.
Conclusion
Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: Cobra flags fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.
Self-check: write a test that drives cobra flags to the failing condition and assert the system degrades gracefully. Then scope CLI flags is a property you maintain, not a feature you hope for.
Sources: Cobra flags, pflag
FAQ
- Persistent vs local?
- Persistent is inherited by subcommands. Local is scoped to one.
- Required flags?
- Mark with MarkFlagRequired. Cobra errors on missing.
Report a problem with this articleOpens a pre-filled issue on GitHub.