DEV Community

42Atomys
42Atomys

Posted on • Edited on

✨ How will Go Generics in Go 1.18 change your life? 💜

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")
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

And now let's update our Join function declaration

func JoinNumeric[E Numeric](elemets ...E) []E
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
lightsouldev profile image
Nurzhan Kozhanov

Very nice, thanks
Little typo:
func JoinString(elements ...string) []int {
return elements
}

it's supposed to be a "[]string" type

Collapse
 
42atomys profile image
42Atomys

Oups ! Fixed 🤭😇

Collapse
 
deadlysyn profile image
Mike Hoskins

keep up the good work, nicely done!

Collapse
 
agusescobar profile image
agustin

so much clear, thanks for share <3 <3