-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
Using the cobra skeleton generator, root.go
contains a global variable cfgFile
, and the pointer is passed to rootCmd.PersistentFlags().StringVar()
. This may result in data race in some circumstances. In my case it happened with go test -race -v
and a table based test containing three sub tests.
=== RUN TestExecution
=== RUN TestExecution/successful_date
==================
WARNING: DATA RACE
Write at 0x000001513440 by goroutine 13:
github.com/spf13/pflag.newStringValue()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/string.go:7 +0x44
github.com/spf13/pflag.(*FlagSet).StringVar()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/string.go:37 +0x36
gitlab.com/sterndata/cronjobd/cmd.NewRootCommand()
/home/ep/src/monolith/go/cronjobd/cmd/root.go:29 +0x187
gitlab.com/sterndata/cronjobd_test.TestExecution.func2()
/home/ep/src/monolith/go/cronjobd/integration_test.go:62 +0x40e
testing.tRunner()
/usr/lib/go/src/testing/testing.go:1123 +0x202
[...]
[...]
==================
==================
WARNING: DATA RACE
Write at 0x00c00015c710 by goroutine 13:
github.com/spf13/viper.(*Viper).SetConfigName()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/viper.go:1931 +0x389
github.com/spf13/viper.SetConfigName()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/viper.go:1928 +0x357
gitlab.com/sterndata/cronjobd/cmd.initConfig()
/home/ep/src/monolith/go/cronjobd/cmd/root.go:62 +0x356
github.com/spf13/cobra.(*Command).preRun()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:880 +0x81
github.com/spf13/cobra.(*Command).execute()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:816 +0x1b5
github.com/spf13/cobra.(*Command).ExecuteC()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:958 +0x4b2
github.com/spf13/cobra.(*Command).ExecuteC()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:907 +0xaa8
github.com/spf13/cobra.(*Command).Execute()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:895 +0x4d7
github.com/spf13/cobra.(*Command).ExecuteContext()
/home/ep/go/pkg/mod/github.com/spf13/[email protected]/command.go:888 +0x4ce
gitlab.com/sterndata/cronjobd_test.TestExecution.func2()
/home/ep/src/monolith/go/cronjobd/integration_test.go:65 +0x48d
testing.tRunner()
/usr/lib/go/src/testing/testing.go:1123 +0x202
I worked around the problem by removing cfgFile
, func initConfig
and func init
from root.go
. CLI flags suffice for me, so that fits the bill.
The snake could be defanged by removing the need for global variables and func init
s. In my app I replaced rootCmd
and the other *cobra.Command
globals with NewXXXCommand functions:
func NewRootCommand() *cobra.Command {
return &cobra.Command
// ...
}
}
func NewServeCommand(parent *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
// ...
}
parent.AddCommand(cmd)
// cmd.Flags().String(...)
return cmd
}
This requires main
to grow a bit:
func main() {
rootCmd := cmd.NewRootCommand()
cmd.NewRunCommand(rootCmd)
cmd.NewServeCommand(rootCmd)
cmd.Execute(rootCmd)
}
It improves testability, because I can construct the commands in my test file without worrying about global state scattered across a few files.
func TestExecution(t *testing.T) {
rootCmdForServe := cmd.NewRootCommand()
rootCmdForServe.SetArgs([]string{"serve"})
rootCmdForServe.SetErr(ioutil.Discard)
serveCmd := cmd.NewServeCommand(rootCmdForServe)
go func() {
err := serveCmd.ExecuteContext(...)
if err != nil {
t.Fatalf("Failed executing serve command: %v", err)
}
}()
// ...
}
This approach has the downside that main
needs to grow with each command, because NewXXXCommand adds the child command to the parent. One might argue, adding child to parent needs to happen anyway, and it is better to do it in one function than scatter it across many init
functions.