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/pflag@v1.0.5/string.go:7 +0x44
github.com/spf13/pflag.(*FlagSet).StringVar()
/home/ep/go/pkg/mod/github.com/spf13/pflag@v1.0.5/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/viper@v1.7.1/viper.go:1931 +0x389
github.com/spf13/viper.SetConfigName()
/home/ep/go/pkg/mod/github.com/spf13/viper@v1.7.1/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/cobra@v1.1.1/command.go:880 +0x81
github.com/spf13/cobra.(*Command).execute()
/home/ep/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:816 +0x1b5
github.com/spf13/cobra.(*Command).ExecuteC()
/home/ep/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:958 +0x4b2
github.com/spf13/cobra.(*Command).ExecuteC()
/home/ep/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:907 +0xaa8
github.com/spf13/cobra.(*Command).Execute()
/home/ep/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:895 +0x4d7
github.com/spf13/cobra.(*Command).ExecuteContext()
/home/ep/go/pkg/mod/github.com/spf13/cobra@v1.1.1/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 inits. 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.
Using the cobra skeleton generator,
root.gocontains a global variablecfgFile, and the pointer is passed torootCmd.PersistentFlags().StringVar(). This may result in data race in some circumstances. In my case it happened withgo test -race -vand a table based test containing three sub tests.I worked around the problem by removing
cfgFile,func initConfigandfunc initfromroot.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 inits. In my app I replacedrootCmdand the other*cobra.Commandglobals with NewXXXCommand functions:This requires
mainto grow a bit:It improves testability, because I can construct the commands in my test file without worrying about global state scattered across a few files.
This approach has the downside that
mainneeds 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 manyinitfunctions.