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

Issue

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.

Solution

Generics shine when your generic code runs the same operations on all types you instantiate T with. In case of string and int, there isn’t a common operation to initialize their value from a string. However you still have an option, with two different variations:

Type-switch on T

You use the field with the generic type T in a type-switch, and temporarily set the values into an interface{}/any. Then type-assert the interface back to T in order to return it. 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:

func (f *Field[T]) Get() (T, error) {
    value, ok := os.LookupEnv(f.name)
    if !ok {
        return f.defaultValue, nil
    }
    var ret any
    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
}

Type-switch on *T

You can further simplify the code above and get rid of the empty interface. In this case you take the address of the T-type variable and switch on the pointer types:

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

    var ret T
    switch p := any(&ret).(type) {
    case *string:
        *p = value

    case *int:
        i, _ := strconv.ParseInt(value, 10, 64)
        *p = int(i)
    }
    // ret has the zero value if no case matches
    return ret, nil
}

Note that in both cases you must convert the T value to an interface{}/any in order to use it in a type switch. You can’t type-switch directly on T.

Playground with map to simulate os.LookupEnv: https://go.dev/play/p/LHqizyNL9lP

Answered By – blackgreen

Answer Checked By – Mary Flores (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.