Home > OS >  How to implement type-specific solutions for type sets in Go 1.18?
How to implement type-specific solutions for type sets in Go 1.18?

Time:02-12

given the following code...

type FieldType interface {
    string | int
}

type Field[T FieldType] struct {
    name         string
    defaultValue T
}

func NewField[T FieldType](name string, defaultValue T) *Field[T] {
    return &Field[T]{
        name:         name,
        defaultValue: defaultValue,
    }
}

func (f *Field[T]) Name() string {
    return f.name
}

func (f *Field[T]) Get() (T, error) {
    value, ok := os.LookupEnv(f.name)
    if !ok {
        return f.defaultValue, nil
    }
    return value, nil
}

the compiler shows the error:

field.go:37:9: cannot use value (variable of type string) as type T in return statement

Is there a way to provide implementations for all possible FieldTypes?

Like...

func (f *Field[string]) Get() (string, error) {
    value, ok := os.LookupEnv(f.name)
    if !ok {
        return f.defaultValue, nil
    }
    return value, nil
}

func (f *Field[int]) Get() (int, error) {
    raw, ok := os.LookupEnv(f.name)
    if !ok {
        return f.defaultValue, nil
    }
    value, err := strconv.ParseInt(raw, 10, 64)
    if err != nil {
        return *new(T), err
    }
    return int(value), nil
}

Any hint would be welcome.

CodePudding user response:

This doesn't seem a job for generics, because you still need specific code for each different type. Generics shine when your generic code runs the same operations on all types constrained by T.

In case of the predeclared types string and int, there isn't a common operation to initialize their value from a string. However...

Is there a way to provide implementations for all possible FieldTypes?

The least verbose solution I can think of is using a type switch on the type T. You take advantage of the fact that Field struct already has a field defaultValue of type T:

func (f *Field[T]) Get() (T, error) {
    value, ok := os.LookupEnv(f.name)
    if !ok {
        return f.defaultValue, nil
    }
    var ret interface{}
    switch any(f.defaultValue).(type) {
    case string:
        ret = value

    case int:
        // don't actually ignore errors
        i, _ := strconv.ParseInt(value, 10, 64)
        ret = int(i)
    }
    return ret.(T), nil
}

Notes:

  • you must convert defaultValue to an interface{}/any in order to use it in a type switch. You can't type-switch directly on something of type T.
  • you still must use interface{} as a variable to temporarily hold the return value. Assignment to T is allowed only if the given concrete type is assignable to each specific type in T's type set.
  • you type-assert ret.(T) when returning (remember that ret was an interface{}, not T). Beware that this assertion is unchecked, so it may panic if for some reason ret holds something that isn't in the type set of T

GoTip playground with map to simulate os.LookupEnv: https://gotipplay.golang.org/p/gK9W3AicfMt

CodePudding user response:

Ok, the type switch works if reflections are used.

func (f *Field[T]) Get() (T, error) {
    raw, ok := os.LookupEnv(f.name)
    if !ok {
        return f.defaultValue, nil
    }

    v := reflect.ValueOf(new(T))

    switch v.Type().Elem().Kind() {
    case reflect.String:
        v.Elem().Set(reflect.ValueOf(raw))

    case reflect.Int:
        value, err := strconv.ParseInt(raw, 10, 64)
        if err != nil {
            return f.defaultValue, err
        }
        v.Elem().Set(reflect.ValueOf(int(value)))
    }

    return v.Elem().Interface().(T), nil
}

But better solutions are very welcome ;-)

  • Related