There is a way to do so without using reflection. But it leads to more expressive handling of possible types, check on playground:
import (
"fmt"
"strconv"
"strings"
"golang.org/x/exp/constraints"
)
type Number interface {
constraints.Integer | constraints.Complex | constraints.Float
}
func FormatNumber[T Number](t T, f byte, prec int) (s string) {
switch v := any(t).(type) {
case int:
s = strconv.FormatInt(int64(v), 10)
case int8:
s = strconv.FormatInt(int64(v), 10)
case int16:
s = strconv.FormatInt(int64(v), 10)
case int32:
s = strconv.FormatInt(int64(v), 10)
case int64:
s = strconv.FormatInt(v, 10)
case uint:
s = strconv.FormatUint(uint64(v), 10)
case uint8:
s = strconv.FormatUint(uint64(v), 10)
case uint16:
s = strconv.FormatUint(uint64(v), 10)
case uint32:
s = strconv.FormatUint(uint64(v), 10)
case uint64:
s = strconv.FormatUint(v, 10)
case float32:
s = strconv.FormatFloat(float64(v), f, prec, 32)
case float64:
s = strconv.FormatFloat(v, f, prec, 64)
case complex64:
s = strconv.FormatComplex(complex128(v), f, prec, 64)
case complex128:
s = strconv.FormatComplex(v, f, prec, 128)
default:
panic("unsupported number type")
}
if f >= 'A' && f <= 'Z' {
s = strings.ToUpper(s)
}
return s
}