Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic type conversion in Golang

I got tired to write manual slice conversions like []int32 -> []int64, because there are many situations when you need to do use slice with another type. So I tried to write a generic function for that:

func convertSlice[T1 any, T2 any](t1 []T1) []T2 {
    t2 := make([]T2, len(t1))
    for i := range t1 {
        t2[i] = T2(t1[i])
    }
    return t2
}

and want to use it like

a := []int{1, 2, 3, 4, 5}
var b []int64 = convertSlice[int, int64](a)

But I cant compile it, compiler says that ./prog.go:8:14: cannot convert t1[i] (variable of type T1 constrained by any) to type T2

So, how can I fix that?

Live example: https://go.dev/play/p/YYOLFjYt4mq

Of course, I can write separate functions for every primitive type like:

func convertNumericSlice[T1, T2 constraints.Integer | constraints.Float](t1 []T1) []T2 {}
func convertStringSlice[T1, T2 ~string](t1 []T1) []T2 {}

but that doesnt look like cool generic way at all.

like image 573
user3906572 Avatar asked Jul 28 '26 09:07

user3906572


2 Answers

It is not possible to convert type parameters with arbitrary constraints. The spec mentions that (conversions):

[...] x can also be converted to type T if one of the following conditions applies:

  • Both V and T are type parameters and a value of each type in V's type set can be converted to each type in T's type set.

If both T1 and T2 are constrained with any, both type sets are virtually include any possible type.

Even though a particular instantiation of your convertSlice function may be valid, the compiler can't statically prove that the conversion T2(t1) is always valid. In theory you could instantiate convertSlice[string, chan func()] and then a string obviously can't be converted to a channel of functions.

The only way to write a catch-all function is to use reflection with the CanConvert method, but you would be left with deciding how to handle the case where CanConvert returns false. Panic? Return a zero value?

If you choose to use generics, you are already making a choice towards type safety, so introducing reflection seems counter-intuitive. With generics, you do have to write functions for each set of convertible underlying types. The main special cases are:

Numbers:

type Number interface {
    constraints.Integer | constraints.Float
}

convertNumbers[T1, T2 Number](t1 []T1) []T2 {}

Strings — it is special-cased for byte slices and rune slices, but byte slices and rune slices aren't convertible to each other, so you'd need two functions here):

convertStrings[T1, T2 ~string | ~[]byte](t1 []T1) []T2 {}
// or
convertStrings[T1, T2 ~string | ~[]rune](t1 []T1) []T2 {}

Complex:

convertComplex[T1, T2 ~complex64 | ~complex128](t1 []T1) []T2 {}
like image 199
blackgreen Avatar answered Jul 31 '26 00:07

blackgreen


If you have a type with all number types then it's possible to convert in between.

Try like below:

type Number interface {
    int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

func convertSlice[T1 Number, T2 Number](t1 []T1) []T2 {
    t2 := make([]T2, len(t1))
    for i := range t1 {
        t2[i] = T2(t1[i])
    }
    return t2
}

Go play ground demo

like image 42
Amila Senadheera Avatar answered Jul 31 '26 00:07

Amila Senadheera



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!