English
Cobra PersistentPreRun dependency injection trap: why is your client always nil?

A real-world case: when building CLI tools with Cobra,AddCommands passed in from client Why is it nil? How to solve it?
Have you ever encountered a situation like this:
go
// In AddCommands, the client is clearly passed in, but when the subcommand executes, client is nil.
func (a *Application) AddCommands() *Application {
newOrder := order.NewOrder(a.client, ...) // a.client is nil
a.command.AddCommand(newOrder.GetNode().Command())
return a
}This problem troubled me for an entire day. Today, through a real-world case, we will thoroughly understand the correct approach to dependency injection in Cobra.
I. Problem Scenario
Suppose we are building one named agentctl CLI tool that requires:
Load configuration file (in
PersistentPreRuncompleted in)Initialize gRPC client (depends on configuration file)
The subcommand uses this client to call the remote service.
The code structure is as follows:
go
type Application struct {
config *config.Config
client client.AgentClient // Needs to be injected into the subcommand
command *cobra.Command
}
func (a *Application) BuildCommand() *Application {
a.command = &cobra.Command{
Use: "agentctl",
}
// Initialize client in PersistentPreRunE
a.command.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// 1. Load configuration
// 2. Initialize client
a.client, err = client.NewClient(...)
return err
}
return a
}
func (a *Application) AddCommands() *Application {
// At this point, a.client is still nil
newOrder := order.NewOrder(a.client, a.embeds, a.versionInfo)
a.command.AddCommand(newOrder.GetNode().Command())
return a
}
func main() {
app := NewApplication()
app.BuildCommand()
app.AddCommands().Execute() // PersistentPreRun is executed only when Execute is called
}Question:AddCommands() at Execute() executed before, while client at PersistentPreRunE Initialize in (Execute executed during the period). Therefore AddCommands passed-in client Yes nil.
II. Understand the Execution Order at a Glance
Key contradiction:
AddCommands在 Before command execution Execution (dependency injection timing is too early)clientAt During command execution ofPersistentPreRunInitialized only midway (dependency availability timing is too late)
III. Why does this problem exist?
Cobra's design philosophy is:
Command structure definition(
AddCommands) should be inExecuteCompleted previouslyRuntime initialization(
PersistentPreRun) atExecuteExecute during the periodThere exists between the two Timing Gap
go
// Cobra internal simplified execution flow
func (c *Command) Execute() error {
// 1. The command structure has been built (AddCommands has been executed)
// 2. Start executing the lifecycle
c.persistentPreRun() // Dependencies are initialized only here
c.preRun()
c.run() // A dependency is needed here.
c.postRun()
return nil
}IV. Solution: Lazy Dependency Injection (Getter Pattern)
Core idea:Instead of passing in a value at construction time, pass in a "method for getting the value".
Solution Implementation
1. Modify the Order layer: accept a Getter function
go
// order/order.go
type Order interface {
GetNode() getnode.Commander
}
type order struct {
// Change to a function type, retrieve lazily.
clientGetter func() client.AgentClient
embeds embed.FS
versionInfo *versionInfo.Version
}
// Change the constructor to accept a getter
func NewOrder(clientGetter func() client.AgentClient, embeds embed.FS, versionInfo *versionInfo.Version) Order {
return &order{
clientGetter: clientGetter,
embeds: embeds,
versionInfo: versionInfo,
}
}
func (o *order) GetNode() getnode.Commander {
// Pass in a getter when creating a subcommand.
return getnode.NewCommander(o.clientGetter, o.embeds)
}2. Modify the subcommand layer: retrieve dependencies during Run
go
// order/getnode/getnode.go
type getnode struct {
// Store the getter instead of the client itself
clientGetter func() client.AgentClient
embeds embed.FS
}
func NewCommander(clientGetter func() client.AgentClient, embeds embed.FS) Commander {
return &getnode{
clientGetter: clientGetter,
embeds: embeds,
}
}
func (d *getnode) getCommand() *cobra.Command {
return &cobra.Command{
Use: "getnode",
Run: func(cmd *cobra.Command, args []string) {
// Only acquire the client at Run time (by then PersistentPreRun has already executed)
client := d.clientGetter()
if client == nil {
gologger.Error().Msg("client not initialized")
return
}
// Use client to call remote service
response, err := client.GetNode(cmd.Context(), &pb.GetNodeRequest{...})
// ...
},
}
}3. Modify the main program: pass in the closure
go
// main.go
func (a *Application) AddCommands() *Application {
// Transitive closure, lazy client retrieval
clientGetter := func() client.AgentClient {
return a.client
}
newOrder := order.NewOrder(clientGetter, a.embeds, a.versionInfo)
a.command.AddCommand(newOrder.GetNode().Command())
return a
}Execution timing (after fix)
1. BuildCommand() - Define PersistentPreRunE (not executed)
2. AddCommands() - pass in clientGetter (at this point client is still nil, but that's okay)
3. Execute() starts execution
├── PersistentPreRunE execution
│ └── Initialize a.client (now it has a value)
├── Subcommand Run execution
│ └── client := clientGetter() <- get the initialized client
└── Call remote serviceV. Comparison of Other Possible Solutions
| Plan | Advantages | shortcoming | Recommendation rating |
|---|---|---|---|
| Getter closure (this approach) | Small changes, consistent with the Cobra design | Need to understand the concept of closures. | 5 stars |
| Initialize the client in advance | Simple and direct | Configuration may not be available in advance. | 2 stars |
| Independent initialization method | Clear and controllable, easy to test | Considerable refactoring | 4 stars |
| Dynamically add commands in PersistentPreRun | - | Anti-pattern, breaking design | Not recommended |
VI. Summary
Key points
Execution order is key.:
AddCommandsatPersistentPreRunExecute before; do not use uninitialized dependencies at this time.Delay injection is king.: Use function closures (Getter) to obtain dependencies only when actually needed
Three Principles of Dependency Injection:
Injected at construction time Stable dependencies(such as
embed.FS,aurora)Obtain at runtime dynamic dependency(e.g.
client,config)use Getter function The timing gap between bridge construction time and runtime
Remember in one sentence.
In Cobra,AddCommands only responsible for "building the framework"; the actual "loading" will have to wait until PersistentPreRun After.
Supporting code pattern
go
// Correct dependency injection pattern
type App struct {
client *Client // Runtime initialization
}
// When constructing: pass in Getter
func NewOrder(getter func() *Client) *Order {
return &Order{clientGetter: getter}
}
// At execution: obtain via Getter
func (o *Order) Run() {
client := o.clientGetter() // It has already been initialized at this point.
client.DoSomething()
}