为什么根据我调用 BindPFlag 的位置会出现 nil 指针错误?

问题描述

我最近才开始使用 Go,我遇到了一些 我不确定我是否理解与 Cobra 和 Viper 合作的行为。

这是您获得的示例代码的稍微修改版本 正在运行 cobra init。在 main.go 我有

package main

import (
    "github.com/larsks/example/cmd"
    "github.com/spf13/cobra"
)

func main() {
    rootCmd := cmd.NewCmdRoot()
    cobra.CheckErr(rootCmd.Execute())
}

cmd/root.go 我有

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"

    "github.com/spf13/viper"
)

var cfgFile string

func NewCmdRoot() *cobra.Command {
    config := viper.New()

    var cmd = &cobra.Command{
        Use:   "example",Short: "A brief description of your application",PersistentPreRun: func(cmd *cobra.Command,args []string) {
            initConfig(cmd,config)
        },Run: func(cmd *cobra.Command,args []string) {
            fmt.Printf("This is a test\n")
        },}

    cmd.PersistentFlags().StringVar(&cfgFile,"config","","config file (default is $HOME/.example.yaml)")
    cmd.PersistentFlags().String("name","a name")

  // *** If I move this to the top of initConfig
  // *** the code runs correctly.
    config.BindPFlag("name",cmd.Flags().Lookup("name"))

    return cmd
}

func initConfig(cmd *cobra.Command,config *viper.Viper) {
    if cfgFile != "" {
        // Use config file from the flag.
        config.SetConfigFile(cfgFile)
    } else {
        config.AddConfigPath(".")
        config.SetConfigName(".example")
    }

    config.AutomaticEnv() // read in environment variables that match

    // If a config file is found,read it in.
    if err := config.ReadInConfig(); err == nil {
        fmt.Fprintln(os.Stderr,"Using config file:",config.ConfigFileUsed())
    }

  // *** This line triggers a nil pointer reference.
    fmt.Printf("name is %s\n",config.GetString("name"))
}

代码将在最终调用时出现 nil 指针引用而导致恐慌 fmt.Printf

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x50 pc=0x6a90e5]

如果我将呼叫从 config.BindPFlag 函数NewCmdRoot 命令的顶部,一切运行 没有问题。

这是怎么回事?根据 Viper 文档关于使用 initConfig

和BindEnv一样,绑定方法是不设置值 调用,但是当它被访问时。这意味着您可以尽早绑定 你想要,即使在 init() 函数中。

这几乎就是我在这里所做的。当时我打电话 BindPFlagsconfig.BindPflag 非零,config 非零,并且 cmd 参数已注册

我认为我在使用 name config 中的闭包,但我不知道为什么会这样 导致此故障。

解决方法

我觉得这很有趣,所以我做了一些挖掘和found your exact problem documented in an issue。有问题的线路是这样的:

config.BindPFlag("name",cmd.Flags().Lookup("name"))
//                           ^^^^^^^

您创建了一个持久性标志,但将该标志绑定到 Flags 属性。如果您将代码更改为绑定到 PersistentFlags,即使在 NewCmdRoot 中使用此行,一切都会按预期工作:

config.BindPFlag("name",cmd.PersistentFlags().Lookup("name"))
,

如果我使用 cmd.PersistentFlags().Lookup("name"),我没有任何问题。

    // *** If I move this to the top of initConfig
    // *** the code runs correctly.
    pflag := cmd.PersistentFlags().Lookup("name")
    config.BindPFlag("name",pflag)

考虑到您刚刚注册了 persistent flags(标志将可用于分配给它的命令以及该命令下的每个命令),调用 cmd.PersistentFlags().Lookup("name") 而不是 {{1} 更安全}.

后者返回 cmd.Flags().Lookup("name"),因为只有在调用 nil 时才调用 PersistentPreRun,即 after rootCmd.Execute()
cmd.NewCmdRoot() 级别,标志尚未初始化,即使在某些标志被声明为“持久”之后也是如此。

,

这最终会比乍一看更复杂一些,所以虽然这里的其他答案帮助我解决了问题,但我想添加一些细节。

如果您刚开始使用 Cobra,文档中有一些细微差别并不是特别清楚。让我们从 PersistentFlags 方法的文档开始:

PersistentFlags 返回当前命令中专门设置的持久化 FlagSet。

密钥在...在当前命令中。在我的 NewCmdRoot root 方法中,我们可以使用 cmd.PersistentFlags(),因为 root 命令是当前命令。我们甚至可以在 cmd.PersistentFlags() 方法中使用 PersistentPreRun只要我们不处理子命令

如果我们要重写示例中的 cmd/root.go 以使其包含一个子命令,就像这样...

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var cfgFile string

func NewCmdSubcommand() *cobra.Command {
    var cmd = &cobra.Command{
        Use:   "subcommand",Short: "An example subcommand",Run: func(cmd *cobra.Command,args []string) {
            fmt.Printf("This is an example subcommand\n")
        },}

    return cmd
}

func NewCmdRoot() *cobra.Command {
    config := viper.New()

    var cmd = &cobra.Command{
        Use:   "example",Short: "A brief description of your application",PersistentPreRun: func(cmd *cobra.Command,args []string) {
            initConfig(cmd,config)
        },args []string) {
            fmt.Printf("Hello,world\n")
        },}

    cmd.PersistentFlags().StringVar(
    &cfgFile,"config","","config file (default is $HOME/.example.yaml)")
    cmd.PersistentFlags().String("name","a name")

    cmd.AddCommand(NewCmdSubcommand())

    err := config.BindPFlag("name",cmd.PersistentFlags().Lookup("name"))
    if err != nil {
        panic(err)
    }

    return cmd
}

func initConfig(cmd *cobra.Command,config *viper.Viper) {
    name,err := cmd.PersistentFlags().GetString("name")
    if err != nil {
        panic(err)
    }
    fmt.Printf("name = %s\n",name)

    if cfgFile != "" {
        // Use config file from the flag.
        config.SetConfigFile(cfgFile)
    } else {
        config.AddConfigPath(".")
        config.SetConfigName(".example")
    }

    config.AutomaticEnv() // read in environment variables that match

    // If a config file is found,read it in.
    if err := config.ReadInConfig(); err == nil {
        fmt.Fprintln(os.Stderr,"Using config file:",config.ConfigFileUsed())
    }

    // *** This line triggers a nil pointer reference.
    fmt.Printf("name is %s\n",config.GetString("name"))
}

...我们会发现它在执行 root 命令时有效:

$ ./example
name =
name is
Hello,world

但是当我们运行子命令时它失败

[lars@madhatter go]$ ./example subcommand
panic: flag accessed but not defined: name

goroutine 1 [running]:
example/cmd.initConfig(0xc000172000,0xc0001227e0)
        /home/lars/tmp/go/cmd/root.go:55 +0x368
example/cmd.NewCmdRoot.func1(0xc000172000,0x96eca0,0x0,0x0)
        /home/lars/tmp/go/cmd/root.go:32 +0x34
github.com/spf13/cobra.(*Command).execute(0xc000172000,0xc000172000,0x96eca0)
        /home/lars/go/pkg/mod/github.com/spf13/cobra@v1.1.3/command.go:836 +0x231
github.com/spf13/cobra.(*Command).ExecuteC(0xc00011db80,0xffffffff,0xc0000240b8)
        /home/lars/go/pkg/mod/github.com/spf13/cobra@v1.1.3/command.go:960 +0x375
github.com/spf13/cobra.(*Command).Execute(...)
        /home/lars/go/pkg/mod/github.com/spf13/cobra@v1.1.3/command.go:897
main.main()
        /home/lars/tmp/go/main.go:11 +0x2a

这是因为子命令从根继承了 PersistentPreRun 命令(这就是 Persistent 部分的意思),但是当这个方法运行时,cmd 参数传递给 {{ 1}} 不再是根命令;这是 PersistentPreRun 命令。当我们尝试调用 subcommand 时,它失败了,因为当前命令没有任何与之关联的持久标志。

在这种情况下,我们需要改用 Flags 方法:

Flags 返回适用于该命令的完整 FlagSet(在此处和所有父级声明的本地和持久性)。

这使我们可以访问父母声明的持久标志。

文档中似乎没有明确指出的另一个问题是 cmd.PersistentFlags() 仅在运行命令处理后才可用(即,在您调用 Flags() 之后命令或父母)。这意味着我们可以在 cmd.Execute() 中使用它,但我们不能PersistentPreRun 中使用它(因为该方法在我们处理命令行之前完成)。


TL;博士

  • 我们必须在 NewCmdRoot 中使用 cmd.PersistentFlags(),因为我们正在寻找应用于当前命令的持久标志,并且 NewCmdRoot 中的值将获胜尚不可用。
  • 我们需要在 Flags()(和其他持久命令方法)中使用 cmd.Flags() 因为在处理子命令时,PersistentPreRun 只会在 当前命令上查找持久标志,但不会遍历父母。我们需要改用 PersistentFlags,它将汇总父母声明的持久标志。