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
}
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
}
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
}
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
andn2
as parameters, and returns the sum of the two.
Now for the whacky part.
[N int | float64]
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 {...}
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))
}
3
3.3
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))
default type float64 of 2.1 does not match inferred type int for N
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
}
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
}
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
}
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.
Top comments (0)