Featured image of post One variadic, and I'd already spent it

One variadic, and I'd already spent it

I had a constructor I was rather pleased with. Hand go-tool-base’s root command its props and as many sub-commands as you like, and off it goes. Then I needed to thread some config file paths through it, reached for the obvious “just add a parameter,” and discovered I’d already spent my one variadic with no second one going spare.

The ergonomics I’d happily bought

NewCmdRoot ends in subcommands ...*cobra.Command. That trailing ... is a small luxury: callers write NewCmdRoot(props, build, deploy, status) and never have to think about slices. Variadics are lovely for exactly this, the “and as many of these as you fancy” argument.

The parameter I couldn’t add

Then config arrived, and the root command needed to know about some extra configuration file paths. The instinct is to add a parameter. The instinct is wrong, because there’s nowhere legal to put it.

You can’t write NewCmdRoot(props, configPaths ...string, subcommands ...*cobra.Command). Go allows a function exactly one variadic, and it must be the final parameter. Two variadics results in a compile error before you’ve finished the line (assuming your IDE does compile time checks for you), and fairly so: at the call site, how would Go ever know where the strings stopped and the commands began? So the variadic I’d spent on sub-commands was spent. There wasn’t another to hand.

The choices, and the one I took

You can demote the variadic. Make it subcommands []*cobra.Command and you’re free to add configPaths []string next to it. Correct, and it breaks every existing call: NewCmdRoot(props, build, deploy) becomes NewCmdRoot(props, []string{}, []*cobra.Command{build, deploy}). Uglier at every site, to solve a problem only some callers have.

You can reach for functional options, and for plenty of go-tool-base’s constructors that is exactly what happened. But the root builder is the one everybody calls first, with the simplest signature in the codebase, and I didn’t want the common case lugging option machinery around for the sake of the rare one.

What I actually did was add a second door. From pkg/cmd/root/root.go:

// NewCmdRoot creates the root command with Props wiring and optional subcommands.
func NewCmdRoot(props *p.Props, subcommands ...*cobra.Command) *cobra.Command {
	return NewCmdRootWithConfig(props, []string{}, subcommands...)
}

func NewCmdRootWithConfig(props *p.Props, configPaths []string, subcommands ...*cobra.Command) *cobra.Command {
	// ...
}

The new argument goes in as a plain []string, sat before the variadic, which is perfectly legal: one variadic, still last. Callers who care about config use NewCmdRootWithConfig explicitly, and NewCmdRoot becomes a one-line wrapper that delegates with an empty slice, so every existing caller compiles untouched and none the wiser. Two doors into the same room, granted, but the original door is exactly where everyone left it.

What it comes down to

A trailing variadic is a slot you fill once. It buys gorgeous ergonomics for the “as many as you like” argument, and in exchange it quietly forecloses on ever appending another parameter, because the next one has nowhere to stand. Once it’s spent, new arguments come in as ordinary parameters before the variadic, and the kind thing to do for your callers is to put that behind a second constructor and let the original keep delegating.

So spend the variadic deliberately. Give it to the argument that genuinely wants to be a loose list, not the first one that happens to be plural, because you don’t get a second.

Built with Hugo
Theme Stack designed by Jimmy