为什么我在调用BindPFlag时会出现空指针错误,这取决于我调用的位置?

huangapple go评论143阅读模式
英文:

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")

	// *** 如果我将此行移到initConfig的顶部
	// *** 代码将正确运行。
	config.BindPFlag("name", cmd.Flags().Lookup("name"))

	return cmd
}

func initConfig(cmd *cobra.Command, config *viper.Viper) {
	if cfgFile != "" {
		// 使用命令行标志中的配置文件。
		config.SetConfigFile(cfgFile)
	} else {
		config.AddConfigPath(".")
		config.SetConfigName(".example")
	}

	config.AutomaticEnv() // 读取匹配的环境变量

	// 如果找到配置文件,则读取它。
	if err := config.ReadInConfig(); err == nil {
		fmt.Fprintln(os.Stderr, "Using config file:", config.ConfigFileUsed())
	}

	// *** 此行触发了空指针引用。
	fmt.Printf("name is %s\n", config.GetString("name"))
}

在最后一次调用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函数移动到initConfig函数的顶部,一切都可以正常运行。

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

> 类似于BindEnv,当绑定方法被调用时,值并不会被设置,而是在访问时设置。这意味着您可以尽早进行绑定,甚至在init()函数中。

这几乎与我在这里所做的完全相同。在调用config.BindPflag时,config是非空的,cmd是非空的,并且已经注册了name参数。

我猜测在PersistentPreRun闭包中使用config时可能出了问题,但我不确定为什么会导致此错误。

英文:

I've just recently started working with Go, and I've run into some
behavior working with Cobra and Viper that I'm not sure I understand.

This is a slightly modified version of the sample code you get by
running cobra init. In main.go I have:

package main
import (
"github.com/larsks/example/cmd"
"github.com/spf13/cobra"
)
func main() {
rootCmd := cmd.NewCmdRoot()
cobra.CheckErr(rootCmd.Execute())
}

In cmd/root.go I have:

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"))
}

This code will panic with a nil pointer reference at the final call to
fmt.Printf:

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

If I move the call to config.BindPFlag from the NewCmdRoot
function to the top of the initConfig command, everything runs
without a problem.

What's going on here? According to the Viper docs regarding the use of
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.

That's almost exactly what I'm doing here. At the time I call
config.BindPflag, config is non-nil, cmd is non-nil, and the
name argument has been registered.

I assume there's something going on with my use of config in a
closure in PersistentPreRun, but I don't know exactly why that is
causing this failure.

答案1

得分: 3

我觉得这很有趣,所以我进行了一些调查,并在一个问题中找到了你的确切问题的记录。问题出在这一行代码上:

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

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

config.BindPFlag("name", cmd.PersistentFlags().Lookup("name"))
英文:

I thought this was interesting so I did some digging and found your exact problem documented in an issue. The problematic line is this:

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

You created a persistent flag, but bound the flag to the Flags property. If you change your code to bind to PersistentFlags, everything will work as intended even with this line in NewCmdRoot:

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

答案2

得分: 2

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

如果你刚开始使用Cobra,文档中有一些细微之处可能不太清楚。让我们从PersistentFlags方法的文档开始:

> PersistentFlags返回在当前命令中明确设置的持久FlagSet。

关键在于*...在当前命令中*。在我的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"))
}

...我们会发现当执行根命令时它可以工作:

$ ./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部分的含义),但是当这个方法运行时,传递给PersistentPreRuncmd参数不再是根命令;它是subcommand命令。当我们尝试调用cmd.PersistentFlags()时,它会失败,因为当前命令没有与之关联的任何持久标志。

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

> Flags返回适用于此命令的完整FlagSet(在此命令和所有父命令中声明的本地和持久标志)。

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

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


总结:

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

This ends up being a little more complex than it might appear at first glance, so while the other answers here helped me resolve the problem I'd like to add a little detail.

There are some nuances in the documentation that aren't particularly clear if you're just starting to work with Cobra. Let's start with the documentation for the PersistentFlags method:

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

The key is in ...in the current command. In my NewCmdRoot root method, we can use cmd.PersistentFlags() because the root command is the current command. We can even use cmd.PersistentFlags() in the PersistentPreRun method, as long as we're not processing a subcommand.

If we were to re-write cmd/root.go from the example so that it includes a subcommand, like this...

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"))
}

...we would find that it works when executing the root command:

$ ./example
name =
name is
Hello, world

But it fails when we run the subcommand:

[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

This is because the subcommand inherits the PersistentPreRun command from the root (this is what the Persistent part means), but when this method runs, the cmd argument passwd to PersistentPreRun is no longer the root command; it's the subcommand command. When we try to call cmd.PersistentFlags(), it fails because the current command doesn't have any persistent flags associated with it.

In this case, we need to instead use the Flags method:

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

This gives us access to persistent flags declared by parents.

An additional issue, that doesn't appear to be called out explicitly in the documentation, is that Flags() is only available after command processing has been run (that is, after you call cmd.Execute() on the command or a parent). That means we can use it in PersistentPreRun, but we can't use it in NewCmdRoot (because that method finishes before we process the command line).


TL;DR

  • We have to use cmd.PersistentFlags() in NewCmdRoot because we're looking for persistent flags applied to the current command, and the value from Flags() won't be available yet.
  • We need to use cmd.Flags() in PersistentPreRun (and other persistent commands methods) because when processing a subcommand, PersistentFlags will only look for persistent flags on the current command, but won't traverse parents. We need to use cmd.Flags() instead, which will roll up persistent flags declared by parents.

答案3

得分: 1

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

// *** 如果我将这段代码移到initConfig的顶部
// *** 代码将会正确运行。
pflag := cmd.PersistentFlags().Lookup("name")
config.BindPFlag("name", pflag)

考虑到你刚刚注册了持久标志(该标志将对分配给它的命令以及该命令下的每个命令都可用),调用cmd.PersistentFlags().Lookup("name")比调用cmd.Flags().Lookup("name")更安全。

后者返回nil,因为只有在调用rootCmd.Execute()时才会调用PersistentPreRun,而这是在cmd.NewCmdRoot()之后进行的。
cmd.NewCmdRoot()级别,标志尚未初始化,即使其中一些被声明为“persistent”。

英文:

I don't have any issue if I use 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)

Considering you just registered persistent flags (flag will be available to the command it's assigned to as well as every command under that command), it is safer to call cmd.PersistentFlags().Lookup("name"), rather than cmd.Flags().Lookup("name").

The latter returns nil, since the PersistentPreRun is only called when rootCmd.Execute() is called, which is after cmd.NewCmdRoot().
At cmd.NewCmdRoot() levels, flags have not yet been initialized, even after some were declared "persistent".

huangapple
  • 本文由 发表于 2021年5月29日 00:25:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/67742457.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定