Let's Go Optional Go features › Using generics
Previous · Contents · Next
Chapter 13.2.

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:

In contrast, you probably don’t want to use generics:

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:

File: internal/validator/validator.go
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:

File: cmd/web/handlers.go
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.