I have a validation function Positive, it's works but looks ugly.
type Positiver interface {
decimal.Decimal | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}
//nolint:cyclop
func Positive[T Positiver](value T, name string, errs *[]error) {
addError := func() {
err := fmt.Errorf(`%s %w, but it's %v`, name, failures.ShouldBePositive, value)
*errs = append(*errs, err)
}
const prescision = 8
switch val := any(value).(type) {
case decimal.Decimal:
if val.IsNegative() || val.IsZero() {
err := fmt.Errorf(`%s %w, but it's %s`, name, failures.ShouldBePositive, val.StringFixedBank(prescision))
*errs = append(*errs, err)
}
return
case int:
if val <= 0 {
addError()
}
case int64:
if val <= 0 {
addError()
}
case int32:
if val <= 0 {
addError()
}
case int16:
if val <= 0 {
addError()
}
case int8:
if val <= 0 {
addError()
}
case uint:
if val <= 0 {
addError()
}
case uint64:
if val <= 0 {
addError()
}
case uint32:
if val <= 0 {
addError()
}
case uint16:
if val <= 0 {
addError()
}
case uint8:
if val <= 0 {
addError()
}
case float32:
if val <= 0 {
addError()
}
case float64:
if val <= 0 {
addError()
}
default:
panic(fmt.Sprintf(`%T is not supported type`, val))
}
}
I know that is bad approach to use []error, it's better just to return a wrapped error. But it's a compatibility issue.
I tried to do like this:
func Positive[T Positiver](value T, name string, errs *[]error) {
switch val := any(value).(type) {
case decimal.Decimal:
if val.IsNegative() || val.IsZero() {
err := fmt.Errorf(`%s %w, but it's not`, name, failures.ShouldBePositive)
*errs = append(*errs, err)
}
return
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
if val.(int64) <= 0 {
err := fmt.Errorf(`%s %w, but it's not`, name, failures.ShouldBePositive)
*errs = append(*errs, err)
}
return
case float32, float64:
if val.(float64) < 0 {
err := fmt.Errorf(`%s %w, but it's not`, name, failures.ShouldBePositive)
*errs = append(*errs, err)
}
return
default:
panic(fmt.Sprintf(`%T is not supported type`, val))
}
}
But this approach returns me an error: test panicked: interface conversion: interface {} is int, not int64
What is a better way to compare that value exceeds zero?
Your code doesn't work with "interface conversion: interface {} is int, not int64" because in multiple-type case
the type switch variable val
keeps its original type. See also: golang multiple case in type switch for details.
So in this case you have indeed to assert to something in order to use the order operators. That "something" could be a type parameter.
case int, int8, int16, int32, int64:
if val.(T) <= 0 {
// ...
}
BUT this code still can't use the order operator because the constraint Positive
includes decimal.Decimal
, which doesn't support ordering.
Attempting to write a case
for decimal.Decimal
and a case
for other numeric types won't work well either, because you haven't a good way to reduce the type set of a type constraint. You are back writing one case
for each type. One day Go might allow using union constraints in type switches.
What you can do today is to statically handle decimal.Decimal
and other numeric types differently. You can use the types in the package constraints
to avoid redeclaring everything: Signed
, Unsigned
and Float
. Then a naive function with only numerical types is as simple as this:
func StrictlyPositive[T Signed | Unsigned | Float](v T) bool {
return v > 0
}
BUT with floats, using <
is not enough. Float variables could also be NaN or +/-infinity. You have to decide how to order NaNs; infinities have the sign bit, but IMO it's better to use math.IsInf
to not hide stuff behind the order operators.
So in conclusion I think this function is better off with reflection, which might be slower, but the code doesn't totally suck. The following is a simplified version of your example:
func CheckPositive[T Positive](value T) string {
switch val := any(value).(type) {
case decimal.Decimal:
if val.IsNegative() || val.IsZero() {
return "non positive decimal"
}
case int, int8, int16, int32, int64:
if reflect.ValueOf(val).Int() <= 0 {
return "non positive signed"
}
case uint, uint8, uint16, uint32, uint64:
if reflect.ValueOf(val).Uint() == 0 {
return "non positive unsigned"
}
case float32, float64:
f := reflect.ValueOf(val).Float()
switch {
case math.IsNaN(f):
return "NaN float"
case math.IsInf(f, -1):
return "negative infinite"
case math.IsInf(f, 1):
// do nothing
default:
// not a NaN and not an Infinite
if f <= 0.0 {
return "negative float"
}
}
default:
panic(fmt.Sprintf(`%T is not supported type`, val))
}
return "positive"
}