Now that type parameters are available on golang/go:master
, I decided to give it a try. It seems that I'm running into a limitation I could not find in the Type Parameters Proposal. (Or I must have missed it).
I want to write a function which returns a slice of values of a generic type with the constraint of an interface type. If the passed type is an implementation with a pointer receiver, how can we instantiate it?
type SetGetter[V any] interface {
Set(V)
Get() V
}
// SetGetterSlice turns a slice of type V into a slice of type T,
// with T.Set() called for each entry in values.
func SetGetterSlice[V any, T SetGetter[V]](values []V) []T {
out := make([]T, len(values))
for i, v := range values {
out[i].Set(v) // panic if T has pointer receiver!
}
return out
}
When calling the above SetGetterSlice()
function with the *Count
type as T
, this code will panic upon calling Set(v)
. (Go2go playground) To no surprise, as basically the code created a slice of nil
pointers:
// Count implements SetGetter interface
type Count struct {
x int
}
func (c *Count) Set(x int) { c.x = x }
func (c *Count) Get() int { return c.x }
func main() {
ints := []int{1, 2, 3, 4, 5}
sgs := SetGetterSlice[int, *Count](ints)
for _, s := range sgs {
fmt.Println(s.Get())
}
}
This ideas won't work, and I can't seem to find any simple way to instantiate the pointed value.
out[i] = new(T)
will result in a compile failure, as it returns a *T
where the type checker wants to see T
.*new(T)
, compiles but will result in the same runtime panic because new(T)
returns **Count
in this case, where the pointer to Count
is still nil
.T
will result in a compile failure:func SetGetterSlice[V any, T SetGetter[V]](values []V) []*T {
out := make([]*T, len(values))
for i, v := range values {
out[i] = new(T)
out[i].Set(v) // panic if T has pointer receiver
}
return out
}
func main() {
ints := []int{1, 2, 3, 4, 5}
SetGetterSlice[int, Count](ints)
// Count does not satisfy SetGetter[V]: wrong method signature
}
The only solution I found until now, is to require a constructor function to be passed to the generic function. But this just feels wrong and a bit tedious. Why would this be required if func F(T interface{})() []T
is perfectly valid syntax?
func SetGetterSlice[V any, T SetGetter[V]](values []V, constructor func() T) []T {
out := make([]T, len(values))
for i, v := range values {
out[i] = constructor()
out[i].Set(v)
}
return out
}
// ...
func main() {
ints := []int{1, 2, 3, 4, 5}
SetGetterSlice[int, *Count](ints, func() *Count { return new(Count) })
}
My questions, in order of priority:
Using your text editor, create a file called main.go in the generics directory. You'll write your Go code in this file. Into main.go, at the top of the file, paste the following package declaration. A standalone program (as opposed to a library) is always in package main .
Go already supported a form of generic programming via the use of empty interface types. For example, we can write a single function that works for different slice types by using an empty interface type with type assertions and type switches.
You can simplify the above code to one function with generics, and it will work for all possible data types you pass. Generics would significantly reduce duplicated code in your codebase. You can also write other useful generic functions like map, reduce, filter, and so on for arrays and maps.
The Go compiler can infer the type argument from other arguments. From the command line in the directory containing main.go, run the code. $ go run . Non-Generic Sums: 46 and 62.97 Generic Sums: 46 and 62.97 Generic Sums, type parameters inferred: 46 and 62.97 Generic Sums with Constraint: 46 and 62.97 Nicely done!
In calling the generic function you wrote, you specified type arguments that told the compiler what types to use in place of the function’s type parameters. As you’ll see in the next section, in many cases you can omit these type arguments because the compiler can infer them.
This tutorial introduces the basics of generics in Go. With generics, you can declare and use functions or types that are written to work with any of a set of types provided by calling code. In this tutorial, you’ll declare two simple non-generic functions, then capture the same logic in a single generic function. Create a folder for your code.
While a type parameter’s constraint typically represents a set of types, at compile time the type parameter stands for a single type – the type provided as a type argument by the calling code. If the type argument’s type isn’t allowed by the type parameter’s constraint, the code won’t compile.
Basically you have to add one more type parameter to the constraint to make T
convertible to its pointer type. In its most basic form, this technique looks like the following (with an anonymous constraint):
func Foo[T any, PT interface { *T; M() }]() {
p := PT(new(T))
p.M() // calling method on non-nil pointer
}
Playground: https://go.dev/play/p/L00tePwrDfx
Step by step solution
Your constraint SetGetter
already declares a type param V
, so we slightly modify the example above:
// V is your original type param
// T is the additional helper param
type SetGetter[V any, T any] interface {
Set(V)
Get() V
*T
}
Then you define the SetGetterSlice
function with the type parameter T any
, whose purpose is just to instantiate the constraint SetGetter
.
You will then be able to convert the expression &out[i]
to the pointer type, and successfully call the method on the pointer receiver:
// T is the type with methods with pointer receiver
// PT is the SetGetter constraint with *T
func SetGetterSlice[V any, T any, PT SetGetter[V, T]](values []V) []T {
out := make([]T, len(values))
for i, v := range values {
// out[i] has type T
// &out[i] has type *T
// PT constraint includes *T
p := PT(&out[i]) // valid conversion!
p.Set(v) // calling with non-nil pointer receiver
}
return out
}
Full program:
package main
import (
"fmt"
)
type SetGetter[V any, T any] interface {
Set(V)
Get() V
*T
}
func SetGetterSlice[V any, T any, PT SetGetter[V, T]](values []V) []T {
out := make([]T, len(values))
for i, v := range values {
p := PT(&out[i])
p.Set(v)
}
return out
}
// Count implements SetGetter interface
type Count struct {
x int
}
func (c *Count) Set(x int) { c.x = x }
func (c *Count) Get() int { return c.x }
func main() {
ints := []int{1, 2, 3, 4, 5}
// instantiate with base type
sgs := SetGetterSlice[int, Count](ints)
for _, s := range sgs {
fmt.Println(s.Get()) // prints 1,2,3,4,5 each in a newline
}
}
This becomes more verbose because SetGetterSlice
now requires three type parameters: the original V
plus T
(the type with pointer receivers) and PT
(the new constraint). However when you call the function, you can omit the third one – with type inference, both type params V
and T
required to instantiate PT SetGetter[V,T]
are already known:
SetGetterSlice[int, Count](ints)
Playground: https://go.dev/play/p/gcQZnw07Wp3
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With