gostructpanicgo-cobrapointer-conversion

Procedurally bind struct fields to command line flag values using reflect


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

and my slightly broader question is

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.


Solution

  • 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