This repository was archived by the owner on Aug 31, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e155b96
commit b4c8c26
Showing
1 changed file
with
111 additions
and
0 deletions.
There are no files selected for viewing
111 changes: 111 additions & 0 deletions
111
...osts/2022_09_04-using-go-generics-to-pass-struct-slices-for-interface-slices.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
<!-- | ||
Tags: golang | ||
--> | ||
|
||
# Using Go generics to pass struct slices for interface slices | ||
|
||
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: | ||
|
||
1. Create a conversion function for each individual type which implements the `Human` interface | ||
2. Create a "generic" conversion function using `interface{}` | ||
3. 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](https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#No-parameterized-methods). | ||
|
||
The good new 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! |