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

Why am I getting a nil pointer error depending on where I call BindPFlag?

我最近才开始使用 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]

如果我将呼叫从 NewCmdRoot 移动到 config.BindPFlag 函数到 initConfig 命令的顶部,所有内容 运行s 没问题。

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

Like BindEnv, the value is not set when the binding method is called, but when it is accessed. This means you can bind as early as you want, even in an init() function.

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

我想我在使用 config 时发生了一些事情 在 PersistentPreRun 中关闭,但我不知道为什么会这样 导致此失败。

如果我使用 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") 而不是 cmd.Flags().Lookup("name").

后面的returnsnil,因为PersistentPreRun只有在调用rootCmd.Execute()的时候才会调用,也就是after cmd.NewCmdRoot().
cmd.NewCmdRoot() 级别,标志尚未初始化,即使在某些标志被声明为“持久”之后也是如此。

我觉得这很有趣,所以我做了一些挖掘 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"))

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

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

PersistentFlags returns the persistent FlagSet specifically set in the current command.

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

如果我们重写示例中的 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)
        },
        Run: func(cmd *cobra.Command, 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, 0x96eca0, 0x0, 0x0, 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, 0x0, 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部分的意思),但是当这个方法运行s时,cmd PersistentPreRun 的参数 passwd 不再是 root 命令;这是 subcommand 命令。当我们尝试调用 cmd.PersistentFlags() 时,它失败了,因为 当前命令 没有任何与之关联的持久标志。

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

Flags returns the complete FlagSet that applies to this command (local and persistent declared here and by all parents).

这使我们能够访问父级声明的永久标志。

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


TL;DR

  • 我们必须在 NewCmdRoot 中使用 cmd.PersistentFlags(),因为我们正在寻找应用于 当前命令 的永久标志,以及来自 Flags() 尚不可用。
  • 我们需要在 PersistentPreRun 中使用 cmd.Flags()(以及其他持久命令方法),因为在处理子命令时,PersistentFlags 只会在 当前命令,但不会遍历父级。我们需要使用 cmd.Flags() 来代替,它将汇总父级声明的持久性标志。