With the arrival of a long awaited update for some, or an update like the others for others. Generics in Go are about to be your new code companion in your Go projects.
A concrete example? Yes
For me one of the easiest ways to understand a new feature is to have an example. Until 1.17 (without Generics) having a function that created a slice of an element was easy for a type
but if you wanted to manage several types it was immediately less fun.
// 1.17 without generic
func JoinBool(elements ...bool) []bool {
return elements
}
func JoinInt(elements ...int) []int {
return elements
}
func JoinString(elements ...string) []string {
return elements
}
func main() {
slice := JoinInt(1, 2, 3)
slice2 := JoinBool(true, true, false)
slice3 := JoinString("a", "b", "c")
}
In 1.17, you had to create a function for each type
and call it individually.
🎉 A new foe has appeared : Go Generic
// 1.18 with generics
func Join[E any](elements ...E) []E {
return elements
}
func main() {
slice := Join[int](1, 2, 3)
slice2 := Join[bool](true, true, false)
slice3 := Join[string]("a", "b", "c")
}
In 1.18 the generics allow you to program your types. Yes, I think it's sexy to say it like that!
What am I reading? From Go!
everything between []
is the definition of your types.
In the definition of the function func Join[E any](elements ...E) []E
as well as in the call slice := Join[int](1, 2, 3)
.
The type
is defined when the function is called and used in the function code.
Let's split the function Join
with the call Join[int](1, 2, 3)
func Join[E any](elements ...E) []E {
return elements
}
In the example above I create an E
type which can be of type any
(keyword to say that everything is accepted). If you want a hint of understanding, imagine that at runtime you replace all E
by int
in a dynamic way.
Your function will then run like this if I simplify
// E type become int
func Join(elements ...int) []int {
return elements
}
Now I let you imagine with all the other guys
Do you see it? Do you feel the power of Generics?
any
is the new interface{}
?
Yes, but... No. Until now, if you wanted a function to accept several types, you used the interface{}
hack which allowed you to receive anything. However it was to be used with care! And a type check was mandatory.
Let's take our Join
function coded with interface{}
again
func Join(elements ...interface{}) ([]interface{}, error) {
// Check the interface type
switch t := elements.(type) {
case []int, []string:
return elements, nil
default:
return nil, errors.New("Unsupported type")
}
}
func main() {
slice, err := Join(1, 2, 3)
if err != nil {
panic(err)
}
// slice is now a type of `[]interface{}`
// Not a good idea. A Disaster ! To convert to []int again we needs to apply
// a second for loop
var intSlice = []int{}
for _, v := range slice {
// Cast and check if the type of the interface{} is castable to `int` type
if intCasted, ok := v.(int); ok {
intSlice := append(intSlice, intCasted)
}
}
// intSlice is not usable after a lot of pain...
}
When you use interface{}
, it is mandatory to test your cast. Otherwise it's a crash! The cast is done at runtime which is quite risky for your programs.
🎉 A new foe has appeared : Go Generic
Here they come again! Version 1.18 is much nicer to read now that we know that our types are defined and checked at buildtime. If you use an IDE like VSCode, it will even warn you before building.
any
everywhere !
NO! Never assume to set any
by default! You should only start with the desired types, and as a last resort use any
.
You will say yes, but Atomys, how can I do if my function can receive several types? The answer is simple, you can list as many types as you want:
func JoinNumeric[E int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64](elemets ...E) []E
My function accepts here a generic type E
which could ONLY be of type numeric.
Yikes, but I'll have to write this every time? No, don't worry!
It is possible to create your own type, using interfaces
. And yes, they are not going to disappear.
Let's create our Numeric
type!
type Numeric interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}
And now let's update our Join
function declaration
func JoinNumeric[E Numeric](elemets ...E) []E
WOW! So much cleaner. Yes I'll give you that.
This is a concrete example of what Go Generics can bring in 1.18. But that's not all two new features comparable
and the constraints
package. We will see that in a next article !
Thanks for taking the time to read my article, it was my first one !
🙏 Please don't hesitate to support me with a follow on Github/42Atomys and DEV.to/42Atomys. New Go projects are coming soon.
💜💜
Top comments (4)
Very nice, thanks
Little typo:
func JoinString(elements ...string) []int {
return elements
}
it's supposed to be a "[]string" type
Oups ! Fixed 🤭😇
keep up the good work, nicely done!
so much clear, thanks for share <3 <3