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 FieldType
s?
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 aninterface{}
/any
in order to use it in a type switch. You can't type-switch directly on something of typeT
. - you still must use
interface{}
as a variable to temporarily hold the return value. Assignment toT
is allowed only if the given concrete type is assignable to each specific type inT
's type set. - you type-assert
ret.(T)
when returning (remember thatret
was aninterface{}
, notT
). Beware that this assertion is unchecked, so it may panic if for some reasonret
holds something that isn't in the type set ofT
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 ;-)