Yesterday Go 1.18 was released!
1.18 has been eagerly awaited by the Go Community as it contained the official support of generics in Go as well as a whole host of other features which I hope to cover off in other blog posts in the future. You can read more about the release in the official release notes here.
If you like this blog post and want to support me to write more whilst learning more about Go, you can check out my site bytesizego.com. I have both free and paid courses and writing.
For this blog post, we are going to focus on the following bullet from the notes:
> The syntax for function and type declarations now accepts type parameters.
Type Parameters? What are They?
Type parameters are Go's way to support generics. Generic's allow developers to write "general" code that works for many different data types without having to specify that at the point of code creation. I know that is confusing, so let's look at an example.
Before Go 1.18, lets say we had the following slices we wanted to sum together:
func main() {
intsToAdd := []int{1, 2, 3, 4}
floatsToAdd := []float64{1.1, 2.2, 3.3, 4.4}
}
We would need to write the following code:
func sumInts(nums []int) int {
var res int
for _, num := range nums {
res += num
}
return res
}
func sumFloats(nums []float64) float64 {
var res float64
for _, num := range nums {
res += num
}
return res
}
func main() {
intsToAdd := []int{1, 2, 3, 4}
floatsToAdd := []float64{1.1, 2.2, 3.3, 4.4}
fmt.Println("ints", sumInts(intsToAdd))
fmt.Println("floats", sumFloats(floatsToAdd))
}
Which outputs:
ints 10
floats 11
This works and there is nothing wrong with it at all. In fact, I imagine some teams will choose to continue to write code like the above due to its clarity.
However, it does lead to a lot of very similar functions as you can see.
With the introduction of generics, we can write this much more succinctly as the following:
func main() {
intsToAdd := []int{1, 2, 3, 4}
floatsToAdd := []float64{1.1, 2.2, 3.3, 4.4}
fmt.Println("ints", sumNumbers(intsToAdd))
fmt.Println("floats", sumNumbers(floatsToAdd))
}
type Number interface {
int | int64 | float32 | float64
}
func sumNumbers[n Number](nums []n) n {
var res n
for _, num := range nums {
res += num
}
return res
}
This looks pretty confusing to me since I'm not used to it, but I'm hoping over time I get more comfortable reading code like this.
Let's step through it.
Firstly we declare an interface which is going to be our type constraint:
type Number interface {
int | int64 | float32 | float64
}
Here we are saying anything that is an int, an int64, a float32 or float64 is a Number
. Whenever we reference Number
, it must be one of these things. This is similar to how we have used Go interfaces in the past.
In the square brackets below we add our type constraint Number
and call it n
. This means whenever we reference n
we are referring to the Number
type. The Compiler will do some clever work at compile time to figure out everything else for us.
func sumNumbers[n Number](nums []n)
Now we have told our function that anytime we reference n
we are referencing either an int int64, float32 or float64, we can fill in the rest of the code.
Please note that generics does not make the following valid.
// trying to mix ints and floats
NumsToAdd := []Number{1.1,-3, 2.2, 3.3, 4.4}
This is because our Number
interface contains a constraint
types which means it cannot be used like this (confusing I know).
When should I use Generics?
There is a video from Go team engineer Ian Lance Taylor here which does a great job of walking through use cases. I highly recommend watching it.
In general, it is advised to start with simple functions and only try and write generic functions once you have wrote very similar code 2 or 3 times. In the example above, I would not have considered writing a generic function until I had wrote the sumFloats
function and realised how similar it was to the sumInts
function.
Hope you found this useful! You can find me on twitter here
Top comments (0)