English
What exactly is the difference between Cobra PreRun and PersistentPreRun? Understand the execution order with one diagram.

2026 Latest Version | Includes Complete Code Examples | Resolves 90% of Beginner Confusion
If you have used the Cobra framework, you must have seen these 5 hook functions:PersistentPreRun、PreRun,Run,PostRun,PersistentPostRun.
But do you really understand the difference between them?
Why did the subcommand not execute the parent command's
PreRun?PreRunandPersistentPreRunWho exactly executes first?defined
RunEDo we still need a definition?Run?
Don't rush,One diagram + 6 practical case studies, help you thoroughly understand it once and for all.
1. Understand the complete execution sequence in one diagram
Let’s get straight to the core essentials! The figure below shows the complete call chain of the 5 hooks when you execute a Cobra command:
Important Prerequisites
PreRun,Run,PostRun 系列钩子Only the current command defines Run or RunE will only execute when If a command only serves as a parent container (without a Run function), these hooks will not be triggered.
II. Core Differences Among the 5 Hooks (Comparison Table)
| hook function | Inherit to subcommands? | Execution timing | Typical scenarios | Is there an E version? |
|---|---|---|---|---|
| PersistentPreRun | Yes | Before Run | Read the configuration file, connect to the database, and set the log level | Yes |
| PreRun | No | Before Run | Parameter validation, permission check, input validation | Yes |
| Run | No | Core execution point | The actual business logic of the command | Yes |
| PostRun | No | After Run | Close file handles and print elapsed time statistics. | Yes |
| PersistentPostRun | Yes | After Run | Global audit logs, report monitoring data | Yes |
Key conclusions:
belt Persistent of the meeting Inheritance Give subcommand (like an heirloom)
without Persistent Belongs only to the current command(like personal items)
bring E suffix return
error, recommended to use
III. Breaking down the 5 hooks one by one (with code examples)
3.1 PersistentPreRun / PersistentPreRunE: The Best Place for Global Initialization
effect: Do some global preparatory work before executing the command,All subcommands will inherit.
Typical scenarios:
Read configuration file (e.g.
.env,config.yaml)Initialize database connection
Set global log format (JSON/Text)
Inject common dependencies (such as Logger, Metrics)
Code example:
go
package main
import (
"fmt"
"log"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var rootCmd = &cobra.Command{
Use: "mycli",
Short: My CLI tool,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// 1. Load the configuration file (executed for all subcommands)
viper.SetConfigName("config")
viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("failed to load configuration file: %w", err)
}
// 2. Set the global log format
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("Global configuration loaded")
return nil
},
}
// Subcommand: serve
var serveCmd = &cobra.Command{
Use: "serve",
Start service
RunE: func(cmd *cobra.Command, args []string) error {
port := viper.GetString("server.port")
log.Printf("Service started on port: %s", port)
return nil
},
}
func init() {
rootCmd.AddCommand(serveCmd)
}
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}Execute mycli serve的输出:
Global configuration loaded <- PersistentPreRun executed
Service started on port: 8080 <- Run executesBest Practices
Use consistently
PersistentPreRunE(return error), do not usePersistentPreRunDon't If you perform time-consuming operations (such as complex calculations) in PersistentPreRun, it will execute before every command.
If the subcommand requires Coverage For the parent's PersistentPreRun, simply write the logic directly in your own hook.
3.2 PreRun / PreRunE: Command-specific "security gate"
Effect: Do it before the current command is executed Dedicated verification, will not be inherited by subcommands.
Typical scenario:
Verify whether specific parameters exist or are valid (e.g.
--portIs it between 1 and 65535)Check whether the input file exists
Permission verification (e.g., checking whether the user has operation permissions)
Code example:
go
var startCmd = &cobra.Command{
Use: "start",
Short: "Start a task",
PreRunE: func(cmd *cobra.Command, args []string) error {
// 1. Validate parameters
if len(args) == 0 {
return fmt.Errorf("please specify task name")
}
// 2. Validate port range
port, _ := cmd.Flags().GetInt("port")
if port < 1 || port > 65535 {
return fmt.Errorf("port must be between 1-65535")
}
fmt.Printf("Validation passed, preparing to start task: %s\n", args[0])
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("Executing task: %s\n", args[0])
// Business logic...
return nil
},
}
func init() {
startCmd.Flags().Int("port", 8080, "service port")
rootCmd.AddCommand(startCmd)
}Execute mycli start mytask --port 70000 the output of:
Error: The port must be between 1 and 65535(The Run function will never execute because PreRunE intercepts it in advance.)
Note
PreRun 里的错误会中断执行, Run and PostRun will no longer be executed
Do not confuse
PreRunandPersistentPreRunThe former only validates the current command, while the latter performs global initialization.
3.3 Run / RunE: Core business logic
effect:The vast majority of commands only need to implement this function., it is the "heart" of the command.
Typical scenarios:
Perform file read and write
Call API
Start service
data processing
Code example (prefer using RunE):
go
var deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy application",
RunE: func(cmd *cobra.Command, args []string) error {
// Core deployment logic
fmt.Println("Packaging...")
fmt.Println("Uploading to server...")
fmt.Println("Deployment successful!")
return nil
},
}Standard Edition vs E Edition:
Run: No return value; if an error occurs, can onlylog.Fataloros.Exit, the program exits directlyRunE: Returnerror, Cobra handles it uniformly,Recommended for use
If both are defined Run and RunE,RunE will overwrite Run Only execute RunE.
3.4 PostRun / PostRunE: The cleanup sappers after command execution
effect: Perform cleanup work after the current command finishes executing,will not inherit.
Typical scenario:
Close the file handle
Disconnect the database connection
Print execution time
Send notifications (e.g., Slack messages)
Code example:
go
var backupCmd = &cobra.Command{
Use: "backup",
Backup database
RunE: func(cmd *cobra.Command, args []string) error {
startTime := time.Now()
// Backup logic...
fmt.Println("Backup complete")
return nil
},
PostRunE: func(cmd *cobra.Command, args []string) error {
// Print elapsed time
fmt.Printf("Backup time: %v\n", time.Since(startTime))
return nil
},
}Tips
In PostRun, you can access variables defined in the Run function (via closures or the command context) to collect execution statistics.
3.5 PersistentPostRun / PersistentPostRunE: Global audit log
effect: After all commands have finished executing, perform global cleanup or reporting,All subcommands will inherit.
Typical scenario:
Report audit logs (which user executed what command)
Close global resources (such as closing the database connection pool).
Send monitoring metrics
Code example:
go
var rootCmd = &cobra.Command{
Use: "mycli",
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
// 1. Close the database connection
if db != nil {
db.Close()
log.Println("Database connection closed")
}
// 2. Report audit logs
auditLog := fmt.Sprintf("User %s executed command: %s", os.Getenv("USER"), cmd.Name())
log.Println("Audit log:", auditLog)
return nil
},
}Important rules:PersistentPostRun at PostRun after Execute, the order is:
PreRun -> Run -> PostRun (current command) -> PersistentPostRun (inherited from parent)IV. The 3 Biggest Pitfalls You’re Most Likely to Fall Into (with a Pitfall-Avoidance Guide)
Pitfall 1: Defined RunE but forgot to handle the returned error
error code:
go
var badCmd = &cobra.Command{
Use: "bad",
RunE: func(cmd *cobra.Command, args []string) error {
if err := doSomething(); err != nil {
return err // An error was returned, but it was not handled in the main function.
}
return nil
},
}
func main() {
badCmd.Execute() // ignored the error
}Correct approach:
go
func main() {
if err := badCmd.Execute(); err != nil {
fmt.Printf("Execution failed: %v\n", err)
os.Exit(1)
}
}Pitfall 2: Confusing the override rules for PreRun and PersistentPreRun
Scene: The parent command defines PersistentPreRun, the subcommand defines PreRun.
go
// parent command
rootCmd.PersistentPreRun = func(...) {
fmt.Println("Parent: PersistentPreRun")
}
rootCmd.PreRun = func(...) {
fmt.Println("Parent: PreRun")
}
// subcommand
childCmd.PreRun = func(...) {
fmt.Println("Child: PreRun")
}Execute childCmd output:
Parent: PersistentPreRun <- inherited from parent (because parent was not overridden)
Child: PreRun <- Child's own PreRun (overrides the parent's PreRun)Remember the mnemonic.:
Persistent Series: If the subcommand is not defined, use the parent's.
Non-Persistent Series: A subcommand always uses its own, and the parent's is overridden.
Pitfall 3: The command does not define Run, yet expects PreRun and PostRun to execute.
Incorrect example:
go
var containerCmd = &cobra.Command{
Use: "container",
Short: Container management (just a group, no Run),
PreRun: func(...) { // Will never execute
fmt.Println("Preparing to execute...")
},
}
var childCmd = &cobra.Command{
Use: "list",
Run: func(...) {
fmt.Println("List containers")
},
}
containerCmd.AddCommand(childCmd)Execute container list:
containerCmd.PreRunwill not execute(because containerCmd does not have Run)Only execute
childCmdone's own lifecycle
Solution: Move the PreRun logic to the subcommand, or use PersistentPreRun(Because even if the container command does not have Run, PersistentPreRun will still be inherited by subcommands.)
V. Practical Case: Building a Complete CLI Tool
Let's integrate the above knowledge points and build a real mycli Tool:
go
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Global variable
var startTime time.Time
var db *fakeDB
type fakeDB struct{}
func (d *fakeDB) Close() error {
log.Println("Database connection closed")
return nil
}
var rootCmd = &cobra.Command{
Use: "mycli",
Short: A complete CLI example
Long: "mycli demonstrates the correct usage of all Cobra lifecycle hooks",
// 1. Global initialization
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Record start time
startTime = time.Now()
// Load configuration
viper.SetConfigName("config")
viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
// Initialize database
db = &fakeDB{}
log.Println("Global initialization complete")
return nil
},
// 2. Global finalization (Note: executed after PostRun)
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
// Close the database
if db != nil {
db.Close()
}
// Print total elapsed time
log.Printf("Total elapsed time: %v\n", time.Since(startTime))
return nil
},
}
// Subcommand: init (initialize project)
var initCmd = &cobra.Command{
Use: "init [project-name]",
Initialize new project
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
// Validate project name
if len(args[0]) < 3 {
return fmt.Errorf("Project name must be at least 3 characters")
}
log.Printf("Preparing to initialize project: %s", args[0])
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
projectName := args[0]
log.Printf("Creating project: %s", projectName)
// Business logic...
return nil
},
PostRunE: func(cmd *cobra.Command, args []string) error {
log.Println("Project initialization complete")
return nil
},
}
// Subcommand: deploy (deployment)
var deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy application",
RunE: func(cmd *cobra.Command, args []string) error {
log.Println("Deploying...")
// Business logic...
return nil
},
}
func init() {
rootCmd.AddCommand(initCmd, deployCmd)
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Printf("Execution failed: %v\n", err)
os.Exit(1)
}
}Execute mycli init myapp Output:
2026/08/31 10:00:00 Global initialization completed <- PersistentPreRun
2026/08/31 10:00:00 Preparing to initialize project: myapp <- PreRun (subcommand)
2026/08/31 10:00:01 Creating project: myapp <- Run
2026/08/31 10:00:02 Project initialization completed <- PostRun (subcommand)
2026/08/31 10:00:02 Database connection closed <- PersistentPostRun
2026/08/31 10:00:02 Total elapsed time: 2.001s <- PersistentPostRunA perfect demonstration of the complete execution chain!
VI. Summary of Best Practices
Recommended practice
| Scene | Recommendation hook | reason |
|---|---|---|
| Read the configuration file and connect to the database | PersistentPreRunE | All commands are required; avoid code duplication. |
| Parameter validation, permission checking | PreRunE | Only applies to the current command and does not pollute the global scope. |
| Core business logic | RunE | Return an error and handle it gracefully |
| Close connection, print elapsed time | PostRunE | Cleanup specific to the current command |
| Global audit log | PersistentPostRunE | All commands are required and should be finalized uniformly. |
| All hooks | Use XxxE Version | Unified error handling makes code more robust. |
Practices to avoid
Do not perform time-consuming operations in PreRun.(e.g., calling an external API), it will block before Run
Don't modify global state in PreRun, making side effects difficult to troubleshoot
Do not mix the regular version and the E version.(if defined simultaneously
PreRunandPreRunE), the E version will overwrite the standard versionDo not use in hooks.
os.Exit(1), it should return an error and let the upper layer handle itDon't put business logic in PersistentPreRun, it should only perform initialization
VII. Conclusion
Let's review the core knowledge points:
Execution order:
PersistentPreRun->PreRun->Run->PostRun->PersistentPostRunInheritance rules: bring
PersistentThose with it will be inherited by subcommands; those without it belong only to the current command.Error handling: Prioritize use
XxxEVersion (returns error)Precondition:
PreRun/PostRunOnly the current command definesRun/RunEwill only executeOverride Rules: The subcommand defines its own
PreRun, parent'sPreRunwas overwritten (butPersistentPreRunstill execute)
Remember it in one sentence.:
Persistent is a family heirloom (inherited); without it, it is a personal item (not inherited); E is a gentleman (elegantly returns an error); without E, it is a brute (exits directly).