使用反射将结构字段以程序方式绑定到命令行标志值

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

Procedurally bind struct fields to command line flag values using reflect

问题

我有几个配置结构,我想要自动解析成可接受的命令行标志(以便用户可以通过命令行界面进行覆盖)。考虑到这些结构可能随时间而演变,并且其中一个结构是interface{},反射似乎是正确的方法。我只需要解析字符串、整数和浮点数。我已经实现了以下功能:

func ReconGenerateFlags(in *ReconConfig, cmd *cobra.Command) {

	for _, f := range reflect.VisibleFields(reflect.TypeOf(*in)) {

		group_name := f.Name

		v := reflect.ValueOf(in).Elem().FieldByName(f.Name).Elem() // Return the concrete substructures pointed to by "in"
		sub_fields := reflect.VisibleFields(v.Type())

		for _, sub_f := range sub_fields {

			tag_name := sub_f.Name
			sub_v := v.FieldByName(tag_name)
			full_flag_name := strings.ToLower(group_name) + "." + strings.ToLower(tag_name)

			switch s := sub_v.Type().String(); s {
			case "string":
				ptr := (*string)(unsafe.Pointer(sub_v.UnsafeAddr()))
				cmd.Flags().StringVar(ptr, flag_name, "", "")
			case "int":
				ptr := (*int)(unsafe.Pointer(sub_v.UnsafeAddr()))
				cmd.Flags().IntVar(ptr, flag_name, 0, "")
			//case "float64":
			//	ptr := (*float64)(unsafe.Pointer(sub_v.UnsafeAddr()))
			//	cmd.Flags().Float64Var(ptr, flag_name, 0.0, "")
			default:
				fmt.Printf("unrecognized type in config setup: %s\n", s)
			}

		}

	}
}

但是当我取消注释float64的部分时,会出现恐慌错误:

panic: reflect.Value.UnsafeAddr of unaddressable value

goroutine 1 [running]:
reflect.Value.UnsafeAddr(...)
	/usr/local/go/src/reflect/value.go:2526

所以,我的具体问题是:

  • "有没有办法使float64正常工作?"

而我稍微广泛的问题是:

  • "有没有更好的反射方法,不需要使用不安全指针转换?"

我更希望完全遵守类型系统,但如何在反射中实现这一点并不明显。另一种选择似乎是使用代码生成,我希望避免这样做,但如果需要的话也可以应对。

提前感谢您的任何建议!

英文:

I have a several configuration structures that I want to automatically parse into accepted command line flags (to allow a user to override them via CLI). Given that the structures are likely to evolve over time, and one of the structures is an interface{}, reflection seems to be the correct approach. I only need to parse strings, ints, and float64s. I've gotten the following working:

func ReconGenerateFlags(in *ReconConfig, cmd *cobra.Command) {

	for _, f := range reflect.VisibleFields(reflect.TypeOf(*in)) {

		group_name := f.Name

		v := reflect.ValueOf(in).Elem().FieldByName(f.Name).Elem() // Return the concrete substructures pointed to by "in"
		sub_fields := reflect.VisibleFields(v.Type())

		for _, sub_f := range sub_fields {

			tag_name := sub_f.Name
			sub_v := v.FieldByName(tag_name)
			full_flag_name := strings.ToLower(group_name) + "." + strings.ToLower(tag_name)

			switch s := sub_v.Type().String(); s {
			case "string":
				ptr := (*string)(unsafe.Pointer(sub_v.UnsafeAddr()))
				cmd.Flags().StringVar(ptr, flag_name, "", "")
			case "int":
				ptr := (*int)(unsafe.Pointer(sub_v.UnsafeAddr()))
				cmd.Flags().IntVar(ptr, flag_name, 0, "")
			//case "float64":
			//	ptr := (*float64)(unsafe.Pointer(sub_v.UnsafeAddr()))
			//	cmd.Flags().Float64Var(ptr, flag_name, 0.0, "")
			default:
				fmt.Printf("unrecognized type in config setup: %s\n", s)
			}

		}

	}
}

But when I uncomment the float64 block I get a panic:

panic: reflect.Value.UnsafeAddr of unaddressable value

goroutine 1 [running]:
reflect.Value.UnsafeAddr(...)
	/usr/local/go/src/reflect/value.go:2526

So, my concrete question is

  • "Is there a way to make this work for float64s?",

and my slightly broader question is

  • "Is there a better approach with reflection that wouldn't require the unsafe pointer casting?"

I'd much prefer to fully respect the type system, but it's not obvious how to do this with reflection. The other alternative seems like it would be with code generation, which I'd like to avoid, but can wrangle if needed.

Thanks in advance for any input!

答案1

得分: 1

如果我正确理解了你的要求,那么不需要使用unsafe。要获取字段的指针,你可以使用Value.Addr方法和type assertions来获取具体类型。

func GenerateFlags(in interface{}, fs *flag.FlagSet, names []string) {
	rv := reflect.ValueOf(in)
	if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Struct {
		return // 如果不是指向结构体的指针,则退出
	}

    rv = rv.Elem()
	rt := rv.Type()
	for i := 0; i < rt.NumField(); i++ {
		sf := rt.Field(i)
		fv := rv.Field(i)
		name := strings.Join(append(names, strings.ToLower(sf.Name)), ".")

		switch fv.Type() {
		case reflect.TypeOf(string("")):
			p := fv.Addr().Interface().(*string)
			fs.StringVar(p, name, "", "")
		case reflect.TypeOf(int(0)):
			p := fv.Addr().Interface().(*int)
			fs.IntVar(p, name, 0, "")
		case reflect.TypeOf(float64(0)):
			p := fv.Addr().Interface().(*float64)
			fs.Float64Var(p, name, 0, "")
		default:
			names := append([]string{}, names...)
			GenerateFlags(fv.Interface(), fs, append(names, strings.ToLower(sf.Name)))
		}
	}
}

https://go.dev/play/p/1F2Kyo0cBuj

英文:

If I understood your requirements correctly then there's NO need to use unsafe. To get a pointer to a field you can just use the Value.Addr method and type assertions to get the concrete type.

func GenerateFlags(in interface{}, fs *flag.FlagSet, names []string) {
	rv := reflect.ValueOf(in)
	if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Struct {
		return // exit if not pointer-to-a-struct
	}

    rv = rv.Elem()
	rt := rv.Type()
	for i := 0; i &lt; rt.NumField(); i++ {
		sf := rt.Field(i)
		fv := rv.Field(i)
		name := strings.Join(append(names, strings.ToLower(sf.Name)), &quot;.&quot;)

		switch fv.Type() {
		case reflect.TypeOf(string(&quot;&quot;)):
			p := fv.Addr().Interface().(*string)
			fs.StringVar(p, name, &quot;&quot;, &quot;&quot;)
		case reflect.TypeOf(int(0)):
			p := fv.Addr().Interface().(*int)
			fs.IntVar(p, name, 0, &quot;&quot;)
		case reflect.TypeOf(float64(0)):
			p := fv.Addr().Interface().(*float64)
			fs.Float64Var(p, name, 0, &quot;&quot;)
		default:
			names := append([]string{}, names...)
			GenerateFlags(fv.Interface(), fs, append(names, strings.ToLower(sf.Name)))
		}
	}
}

https://go.dev/play/p/1F2Kyo0cBuj

huangapple
  • 本文由 发表于 2022年7月7日 08:48:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/72891199.html
匿名

发表评论

匿名网友

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

确定