DEV Community

Jacob Kim
Jacob Kim

Posted on

Intro to Generics in Go

Generics is a topic that many people have opinions about. Some people say that the lack of generics is why they can't write in Go, while some say that the lack of generics makes the code simple and elegant. Whichever way you think, I think it is important that you know how to use it before having any opinions about it. It doesn't make a lot of sense to me to blindly follow some zealous users' opinions on the r/Golang subreddit, although I understand how fun it is to jump on the bandwagon. That said, let's learn about one of the newer, more substantial additions to the Go programming language.

What is generic programming?

Generic programming is a paradigm in which developers try to use a generic function that encompasses many different types. In a strongly typed language, it is common to see that a function enforces a specific parameter type. For example, take a look at the code below:

func addInts(a, b int) int {
    return a+b
}
Enter fullscreen mode Exit fullscreen mode

addInts is a function that takes two int values a and b. If you try to pass something other than an int, your code will throw an error. We need to write another function to handle different types in this case.

func addInts(a, b int) int {
    return a+b
}

func addFloats(a, b float64) float64 {
    return a+b
}
Enter fullscreen mode Exit fullscreen mode

addFloats is a function that takes two float64 values a and b. With these two functions, we can add integers and floats.

Although the above code works, it doesn't spark joy in many developers' hearts. Do we really need to write two different functions with the exact same logic just because the type is slightly different? It's more understandable if we want to handle completely different types such as an int and a string, but int and float64? Really? Surely there is a more elegant way to handle this and not repeat ourselves.

And this is where generic functions come into play.

How do we use it?

Let's create a generic version of the above functions such that we can add an int or a float64.
The function doesn't look very different but has some notable changes.

func genericAddNums[N int | float64](n1, n2 N) N {
    return n1+n2
}
Enter fullscreen mode Exit fullscreen mode

Don't panic! I know it looks weird, and it most definitely looked weird to me at first too. But trust me, it all makes sense. Let's take this function apart.

  • genericAddNums is the name of the function.

  • The function takes n1 and n2 as parameters, and returns the sum of the two.

Now for the whacky part.

[N int | float64]
Enter fullscreen mode Exit fullscreen mode

This portion of the function is called the "type parameter". What it does is that it creates an arbitrary type N that can either be an int or a float64 but not simultaneously. The point of a generic function is to write more elegant code by reducing rewrites, thus keeping the code DRY (aka don't repeat yourself). We do this by allowing our function to take different types, represented by N.

(n1, n2 N) N {...}
Enter fullscreen mode Exit fullscreen mode

This means that our parameters n1 and n2 are of type N, and the function returns a value of type N.

Let's see how this works in the context of the whole code.

package main

import "fmt"

func genericAddNums[N int | float64](n1, n2 N) N {
    return n1+n2
}

func main() {
    fmt.Println(genericAddNums(1, 2))
    fmt.Println(genericAddNums(1.1, 2.2))
}
Enter fullscreen mode Exit fullscreen mode
3
3.3
Enter fullscreen mode Exit fullscreen mode

Pretty straightforward, right? We can see that genericAddNums can be used to add two int's or two float64's. But what if we tried to add an int and a float64?

fmt.Println(genericAddNums(1, 2.2))
Enter fullscreen mode Exit fullscreen mode
default type float64 of 2.1 does not match inferred type int for N
Enter fullscreen mode Exit fullscreen mode

We get the above error. When we pass 1 to the genericAddNums first, the code infers N to be an int. Even though N can be either an int or a float64, it cannot be both simultaneously. Since Go is a statically typed language, cross-type operations are not allowed. N is like a ? box in the Super Mario series. It can be any item, but if you roll a mushroom, it can't be any other item simultaneously.

A couple of tips

It can be tedious to write out individual types to cover. For example, let's say that I want to make a generic function that takes in all comparable types.

Quick disclaimer: in Go, there are certain types that are deemed "comparable". This means that they are allowed to be compared via comparison operators such as == or !=. You can read more about this in The Go Programming Language Specification.

TLDR, this is a list of comparable types:

  • booleans

  • numbers

  • strings

  • pointers

  • channels

  • arrays of comparable types

  • structs whose fields are all comparable types

How do we define this function? We could do something like this:

func thisIsBad[V bool | int | float32 | float64 | string](datum V) V {
    fmt.Println(datum)
    return datum
}
Enter fullscreen mode Exit fullscreen mode

But this looks ridiculous. Fortunately, Go has a special built-in type called comparable that helps us deal with this mess. It's basically an interface that all comparable types implement. Read more about this here.

func thisIsGood[V comparable](datum V) V {
    fmt.Println(datum)
    return datum
}
Enter fullscreen mode Exit fullscreen mode

Much cleaner, right? But sometimes, you might want a very specific subset of types that you want to accept, but don't want to write it out all the time. How do we do this? Well, remember how comparable is just an interface? It turns out that we can take a page from this book and make our own interface.

type MyTypes interface {
    int | float64 | string
}

func thisSparksJoy[V MyTypes](datum V) V {
    fmt.Println(datum)
    return datum
}
Enter fullscreen mode Exit fullscreen mode

Instead of typing out [V int | float64 | string], we can just type [V MyTypes]. It's just a nifty way to keep things in manageable bits.

Conclusion

I hope this guide was easier to digest than the official Go documentation of generics. It's a pretty cool way to write software, and I'm currently trying to use it in my own projects too.

We learned about why we want to use generic functions, and how to write them in Go. We also learned how to type less and define our custom types. Generics is something that many Gophers love or hate. Lovers praise its flexibility and that it is easier for developers to use other languages to transition to Go. Haters worry that people will overuse generic functions in places where it's not needed, slowing down the program and making code harder to read. I tend to be a purist in many areas of life, and I love the idea of using pre-existing tools to do my job efficiently. I also love the Go philosophy of keeping code simple and easy to read, even at a cost of more typing and repetition. However, I think generics will provide new ways to do things. Developers are creative, and we will most definitely run into situations where using generic functions is the easiest way to solve a problem. All in all, I am in favor of generics, as long as I don't fall down the rabbit hole of viewing every problem as a nail to hammer in with generics. Say no to drugs ;)

Also, thank you all for waiting! I'm back in school now, and my initial plan was to keep writing during school. However, I bit off more than I could chew, and markedly failed at accomplishing this. I made some changes to the schedule so that I can spend some time writing every day, and this will hopefully help me produce more content. I don't want to let my readers down again, and I genuinely want to produce helpful content for newbies and refreshers for seasoned veterans. That being said, I should've updated my status, and I apologize to my readers for not doing so.

See you next time :)

You can also read this post on Medium.

Latest comments (0)