When will generic specification be supported in Go?

Generic specification is supported in C++ and Rust, but why not Go?

What about generic type assertion?

func FormatNumber[T Number](t T, f byte, prec int) (s string) {
	defer func() { recover() }()
	switch v := reflect.ValueOf(t); v.Kind() {
	case reflect.Int, reflect.Int8, reflect.Int16,
		reflect.Int32, reflect.Int64:
		s = strconv.FormatInt(v.Int(), f, prec)
	case reflect.Uint, reflect.Uint8, reflect.Uint16,
		reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		s = strconv.FormatUint(v.Uint(), f, prec)
	case reflect.Float32:
		s = strconv.FormatFloat(v.Float(), f, prec, 32)
	case reflect.Float64:
		s = strconv.FormatFloat(v.Float(), f, prec, 64)
	case reflect.Complex64:
		s = strconv.FormatComplex(v.Complex(), f, prec, 64)
	case reflect.Complex128:
		s = strconv.FormatComplex(v.Complex(), f, prec, 128)
	default:
		s = v.String()
	}
	if f >= 'A' && f <= 'Z' {
		s = ToUpper(s)
	}
	return s
}

becomes

func FormatNumber[T Number](t T, f byte, prec int) (s string) {
	switch v := t.(type) {
	case ~int, ~int8, ~int16, ~int32, ~int64:
		s = strconv.FormatInt(int64(v), f, prec)
	case ~uint, ~uint8, ~uint16, ~uint32, ~uint64, ~uintptr:
		s = strconv.FormatUint(uint64(v), f, prec)
	case ~float32:
		s = strconv.FormatFloat(float64(v), f, prec, 32)
	case ~float64:
		s = strconv.FormatFloat(float64(v), f, prec, 64)
	case ~complex64:
		s = strconv.FormatComplex(complex128(v), f, prec, 64)
	case ~complex128:
		s = strconv.FormatComplex(complex128(v), f, prec, 128)
	}
	if f >= 'A' && f <= 'Z' {
		s = ToUpper(s)
	}
	return s
}

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
}
1 Like

We want to group named types of the same base type together, without needing to rewrite the switch statement when new named numeric type are added.

Can you expand on new named numeric types being added? Like is your project implementing new numeric types? If so, you would control them and you could always just implement the stringer interface:

type NewNumber int64

func (n NewNumber) String() string {
	return "the number"
}
//... and then in your switch
default:
	s = fmt.Sprint(v) // Default is to use stringer interface

And if you wanted more control you could just define and implement your own interface:

type NewNumber int64

type Formatter interface {
	Format(base int) string
}

func (n NewNumber) Format(base int) string {
	return strconv.FormatInt(int64(n), base)
}
// ... later in your switch statement
case Formatter:
	s = v.Format(prec)

At any rate, I don’t think there’s a direct way to do what you want to do at the moment. See this issue and final comment:

2 Likes

I think you could change the code from a type switch to using a switch with explicit casts, to group them together. vUint, ok := v.(uint64) will give ok as true and the value for all types which can be cast to uint64