Using generics
Go 1.18 was the first version of the language to support generics — also known by the more technical name of parametric polymorphism.
Very broadly, the new generics functionality allows you to write code that works with different concrete types.
For example, in older versions of Go, if you wanted to check whether a []string
slice and an []int
slice contained a particular value you would need to write two separate functions — one function for the string
type and another for the int
. A bit like this:
func containsString(v string, s []string) bool { for i, vs := range s { if v == vs { return true } } return false } func containsInt(v int, s []int) bool { for i, vs := range s { if v == vs { return true } } return false }
Now, with generics, it’s possible to write a single contains()
function that will work for string
, int
and all other comparable types. The code looks like this:
func contains[T comparable](v T, s []T) bool { for i := range s { if v == s[i] { return true } } return false }
If you’re not yet familiar with generics in Go, there’s a lot of great information available which explains how generics works and walks you through the syntax for writing generic code.
To get up to speed, I highly recommend reading the official Go generics tutorial, and also watching the first 15 minutes of this video to help consolidate what you’ve learnt.
Rather than duplicating that same information here, instead I’d like to talk briefly about a less common (but just as important!) topic: when to use generics.
When to use generics
For now at least, you should aim to use generics judiciously and cautiously.
I know that might sound a bit boring, but generics are a new language feature and best-practices around writing generic code are still being established. If you work on a team, or write code in public, it’s also worth keeping in mind that not all other Go developers will necessarily be familiar with how generic code works yet.
You don’t need to use generics, and it’s OK not to.
But even with those caveats, writing generic code can be really useful in certain scenarios. Very generally speaking, you might want to consider it when:
- You find yourself writing repeated boilerplate code for different data types. Examples of this might be common operations on slices, maps or channels — or helpers for carrying out validation checks or test assertions on different data types.
- You are writing code and find yourself reaching for the
any
(emptyinterface{}
) type. An example of this might be when you are creating a data structure (like a queue, cache or linked list) which needs to operate on different types.
In contrast, you probably don’t want to use generics:
- If it makes your code harder to understand or less clear.
- If all the types that you need to work with have a common set of methods — in which case it’s better to define and use a normal
interface
type instead. - Just because you can. Instead default to writing simple non-generic code, and switch to a generic version later only if it is actually needed.
Using generics in our application
In the next section of the book we’ll start to write tests for our application, and in doing that we’ll generate a lot of duplicate boilerplate code. We’ll use Go’s generics functionality to help us manage this and create some generic helpers for carrying out test assertions on different data types.
But for now, there’s not much in our codebase that would benefit from being made generic. Our application already works — and the code is clear, readable, and doesn’t have much duplication that generics could easily cut out.
Perhaps the only thing really suited to being made generic is the PermittedInt()
function in our internal/validator/validator.go
file.
Let’s go ahead and change this to be a generic PermittedValue()
function, which we can then use each time that we want to check that a user-provided value is in a set of allowed values — irrespective of whether the user-provided value is a string
, int
, float64
or any other comparable type.
Like so:
package validator ... // Replace PermittedInt() with a generic PermittedValue() function. This returns // true if the value of type T equals one of the variadic permittedValues // parameters. func PermittedValue[T comparable](value T, permittedValues ...T) bool { for i := range permittedValues { if value == permittedValues[i] { return true } } return false }
And then we can update our snippetCreatePost
handler to use the new PermittedValue()
function in the validation checks, like this:
package main ... func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) { var form snippetCreateForm err := app.decodePostForm(r, &form) if err != nil { app.clientError(w, http.StatusBadRequest) return } form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank") form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long") form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank") // Use the generic PermittedValue() function instead of the type-specific // PermittedInt() function. form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365") if !form.Valid() { data := app.newTemplateData(r) data.Form = form app.render(w, http.StatusUnprocessableEntity, "create.tmpl", data) return } id, err := app.snippets.Insert(form.Title, form.Content, form.Expires) if err != nil { app.serverError(w, err) return } app.sessionManager.Put(r.Context(), "flash", "Snippet successfully created!") http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) } ...
After making those changes, you should find that your application compiles correctly and continues to function the same way as before.