Using Go generics to pass struct slices for interface slices
Published
Have you ever tried to pass a struct slice into a function which accepts a slice of interfaces? In Go this won't work.
Let's have a quick look at an example. Let's assume we have an interface called Human
and a function called GreetHumans
which accepts a slice of humans and prints their names:
package main
import "fmt"
type Human interface {
Name() string
}
func GreetHumans(humans []Human) {
for _, h := range humans {
fmt.Println("Hello " + h.Name())
}
}
Then we have a separate struct which implements the Human
interface:
type Hero struct {
FirstName string
LastName string
}
func (h Hero) Name() string {
return h.FirstName + " " + h.LastName
}
Nothing unusual so far.
Now one can create an object of type Hero
and pass it into any function that requires an object of Human
. That is expected.
However, the issue occurs when one deals with a slice of Hero
and a function accepts a slice of Human
.
This code won't compile:
func main() {
heroes := []Hero{
{FirstName: "Peter", LastName: "Parker"},
{FirstName: "Bruce", LastName: "Wayne"},
}
GreetHumans(heroes) // <-- Compilation error here
}
Even though all heroes are humans the compiler won't accept this assignment.
You wonder why? Simply because Go doesn't want to hide expensive operations behind convenient syntax:
One has to iterate through a slice of Hero
themselves and convert each object of Hero
explicitly to a Human
in order to cast the entire slice before passing it into the GreetHumans
function. The cost of this conversion becomes immediately visible to the programmer.
What did Go programmers do up until recently?
Well there were mainly three options:
- Create a conversion function for each individual type which implements the
Human
interface - Create a "generic" conversion function using
interface{}
- Create a "generic" conversion function using reflection
Option 1 is extremely tedious and option 2 and 3 provide very weak (or basically no) type safety (because checks would be only performed at runtime).
Since Go 1.18 one can use Generics to tackle the issue.
First we can modify the GreetHumans
function to use Generics and therefore not require any casting at all:
func GreetHumans[T Human](humans []T) {
for _, h := range humans {
fmt.Println("Hello " + h.Name())
}
}
This makes it possible to pass the heroes
slice into the GreetHumans
functions now.
However, sometimes it's not possible to make a function generic. Methods (functions on a type) require all type parameters to be on the type. Parameterized methods are not allowed.
The good news is that even in this case Generics can help to provide a much better conversion function then the previously listed options:
func CastToHumans[T Human](humans []T) []Human {
result := []Human{}
for _, h := range humans {
result = append(result, h)
}
return result
}
Here the generic CastToHumans
function provides type safety at the time of compilation. It still remains an expensive operation but at least it cannot be used in an improper way any longer.
I wasn't sure if this is going to work and I was positively surprised to find out that it does indeed.
It's another neat use case of Generics in Go!