0

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!

John Hoffman
  • 5
  • 1
  • 2
  • Yeah, you definitely shouldn't need `unsafe` for this. I'm struggling to run your code, even without the float. (My attempt: https://go.dev/play/p/lijmyU6weLL). Can you give a full playground example of what you have, including how `ReconConfig` is defined? That would help people give better answers. – Ben Hoyt Jul 07 '22 at 01:27
  • Also, what do you mean by "one of the structures is an `interface{}`"? If this means sometimes you're passed a struct in an `interface{}` value and you don't know what the underlying type is, then reflection may be a good candidate for this. – Ben Hoyt Jul 07 '22 at 01:30
  • @BenHoyt Thanks for the quick response. Apologies: I forget that we have the playground. Would've included it in the original post! Here's an updated playground that's closer to what I'm actually trying to do, but still doesn't run https://go.dev/play/p/mEzmC47StHQ. This one I think actually could run with a little more fussing since I found a bug in my previous version, where I was passing the "DataSpecifiedAtRuntime" by value rather than a pointer to it, which explains the "unaddressable" error. It still begs the question though: there has to be a better way, right?! haha. – John Hoffman Jul 07 '22 at 02:30

1 Answers1

1

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

mkopriva
  • 35,176
  • 4
  • 57
  • 71
  • 1
    Perfect! I'm pretty sure that this is exactly what I was missing (and looking for). I'll give this a try in my code later today hopefully and accept it as the answer once I've tried it! Thank you!! – John Hoffman Jul 07 '22 at 15:45
  • 1
    This ended up being a little bit different than my actual use case, but worked with minimal modification, and ultimately gets at the spirit/principle of the underlying question (how to safely extract concrete pointers from reflect.Value). Thanks again, @mkopriva! – John Hoffman Jul 07 '22 at 19:27