This post was originally written for my mailing list. If you want to get awesome Go tutorials like this delivered to your inbox, sign up for my mailing list!
I previously wrote a crash course on Go's generics where we looked briefly at what generics are, some use cases for them, and some common misconceptions about how they can be used. If you are unfamiliar with Go’s generics, I suggest starting there before reading this post.
In this article we are going to continue the discussion about generics and look at the tilde (~
) character. Specifically, we are going to explore what this means in the context of generics, and see some code samples showing how it can help make generic functions easier to use.
Recently there was a proposal to add a Reverse function to the upcoming slices package. The function itself is pretty simple, but what caught my eye was the ~
character used in the code.
package slices
// Reverse reverses the elements of the slice in place.
func Reverse[S ~[]E, E any](s S) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
In this code, the tilde is used when defining the generic type S
:
S ~[]E
This got me wondering - how many people actually know what this means? I know I didn’t when I first started using generics, and I doubt many others would even know it existed without first seeing it in code, so today I want to explain why it exists and show some examples.
We can start with the proposal where this was first discussed. In it there are some examples and the proposal mentions that:
~T
means the set of all types whose underlying type isT
But what does that actually mean?
Imagine that you have a custom type that isn’t a struct. Instead, it uses a type like int64 as its underlying type. time.Duration does this.
type Duration int64
Having a custom type allows us to add additional methods that otherwise wouldn’t be present on an int64
. It can also help clarify what a type is intended to represent. A function that accepts a time.Duration
is much clearer about what it expects compared to one that takes an int64
as the argument.
func After(d int64) <-chan Time
// vs
// It is a bit clearer that this returns a channel that sends a message
// after a given duration.
func After(d Duration) <-chan Time
Duration’s underlying type is int64
, so if we wanted our generic function to work with a time.Duration, we would need to use the tilde (~). Let’s look at an example of this.
Imagine that we were writing an application and we frequently found ourselves trying to find the minimum of two values. We could address this by writing a generic Min
function.
func Min[T uint | int | int64](a, b T) T {
if a < b {
return a
}
return b
}
With this function we could easily determine which sum is the smallest.
sum1 := 104
sum2 := 222
minSum := Min(sum1, sum2)
Run this on the Go Playground
This code will work until we have two values that don’t match the underlying types we support - uint
, int
, and int64
. As noted before, an example of this is a time.Duration, which isn’t one of those three types.
If we tried to find the minimum of two durations, our code would throw a compiler error.
p1Duration := p1End.Sub(p1Start)
p2Duration := p2End.Sub(p2Start)
minDuration := Min(p1Duration, p2Duration) // Errors at compile time!
See this error on the Go Playground
What makes this error a bit weird or possibly unexpected is the fact that the time.Duration
type actually has an underlying type of int64
.
type Duration int64
Our code knows how to handle int64
s, so why wont it work?
By default Go’s generics are a bit more restrictive in what they allow. When we say a value needs to be either uint
, int
, or int64
, those are all that Go allows. If we want to be a bit more flexible, that is where the tilde (~
) comes into play.
func Min[T ~uint | ~int | ~int64](a, b T) T {
if a < b {
return a
}
return b
}
With the ~
character we are telling the compiler that T
can be any type that is either uint
, int
, int64
, or any type whose underlying type is one of those types. This small change allows our time.Duration
to work as expected. (Yet another Go Playground example 😆)
Luckily, this isn’t something we typically have to concern ourself with, as the constraints package has a number of predefined types for us that all use the tilde by default. If we update our Min
function to use one of these predefined types, and it will support pretty much everything we want to pass in.
func Min[T constraints.Integer](a, b T) T {
if a < b {
return a
}
return b
}
Run this on the Go Playground
Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
Related articles
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.