TL;DR: Every command in a go-tool-base tool receives one argument: a *props.Props. It’s a dependency-injection container holding the logger, config, filesystem, assets, error handler and tool metadata. It is deliberately a concrete struct and deliberately not context.Context, because dependencies should be found by the compiler, not discovered at runtime. The name is a pun, and the pun is the design brief.
Start with the name
The container at the centre of go-tool-base is called Props, and the name is doing real work, so let’s start there.
It is not short for “properties,” though it does hold a few. A prop is the heavy timber or steel beam that stops a structure quietly collapsing. And for anyone who follows the rugby: a prop is the position in the scrum, the broad-shouldered forward whose entire job is to provide structural support so everyone else can get on with the game.
That’s the design brief in a word. Props is not where the clever, flashy work happens. It scores no tries. It is the unglamorous load-bearing thing that holds the framework up so your actual command logic can be the part that’s interesting. If you understand the name, you understand what the struct is for.
What it carries
Props is the single object passed to every command constructor in a go-tool-base tool. It holds the dependencies a command might need:
Tool— metadata about the CLI (name, summary, release source).Logger— the logging abstraction.Config— the loaded configuration container.FS— a filesystem abstraction (afero), so a command never touches the real disk directly.Assets— the embedded-resource manager.Version— build information.ErrorHandler— the centralised error reporter.
A command constructor’s signature is, accordingly, boring on purpose:
func NewCmdExample(p *props.Props) *cobra.Command { ... }
One parameter. Everything the command could need is reachable through it. No globals, no init()-time wiring, no twelve-argument constructors that grow a thirteenth next month.
Why a struct, and not context.Context
Here is the design decision I want to defend, because it’s the one Go developers will raise an eyebrow at. Go already has a well-known way to carry things through a call tree: context.Context. Why not put the logger and the config in the context and pass that?
Because context.Context carries values as interface{}, and that is the wrong trade for dependencies.
Pull a dependency out of a context and you get this:
l := ctx.Value("logger").(logger.Logger) // a runtime type assertion
That line has two ways to hurt you. The key is a bare string, so a typo compiles fine and fails at runtime. The type assertion is unchecked, so if the wrong thing is under that key, your tool panics in front of a user. Neither failure is visible to the compiler. Neither is visible to your IDE. You find out when it breaks.
Pull the same dependency out of Props and you get this:
p.Logger.Info("starting") // a field access
p.Logger is a typed field. If it doesn’t exist, or you’ve used it wrong, the code does not compile. Your IDE autocompletes it. A refactor of the Logger interface lights up every misuse at build time. There is no runtime type assertion because there is no interface{} to assert from.
context.Context is the right tool for what it was designed for: cancellation, deadlines, request-scoped signals that genuinely cross API boundaries. It is the wrong tool for “here are my program’s services,” because it trades away the compiler’s help for flexibility you don’t want. Dependencies should be declared, in a place the compiler checks. Props is that place.
What you get back for it
That one decision pays out in three currencies.
Testability. A command is now a pure function of its Props. To test it, you build a Props with the doubles you want — an in-memory FS instead of the real disk, a no-op Logger, a config you’ve populated by hand — and call the constructor. No global state to reset between tests, no monkey-patching, no init() order to reason about. The dependency is an argument, so the test simply passes a different one.
Consistency. Cross-cutting changes have one place to happen. When the global --debug flag flips the log level, it does so on the Logger inside Props, and because every command reads the logger from the same Props, every command gets the new level. No command can drift, because none of them owns its own copy.
Extensibility. Adding a new framework-wide service is adding a field to one struct. Every command can immediately reach it; none of them needed changing to make it reachable.
The short version
Props is the dependency-injection container at the heart of go-tool-base: one struct, passed to every command, holding the logger, config, filesystem, assets, error handler and tool metadata. It’s a concrete struct rather than a context.Context payload on purpose, because dependencies belong somewhere the compiler can check them, not behind a string key and a runtime type assertion. That single choice buys testability, consistency and easy extension.
The name says it best. Props doesn’t score the tries. It’s the broad-shouldered thing in the scrum that stops the whole framework folding, so the rest of your code is free to play.