Posted on · 5 min to read · 805 words

The latest release of Go (1.18) added generics to the language, besides fuzzing, workspaces and performance improvements. [1] So let's use them to implement the repository pattern, which is a way to abstract data handling from your business logic. When working on REST-like APIs you want to split handling of data from and into some kind of data store (e.g. a database, a file, ...) from business logic. This is where the repository pattern comes in handy. Especially with Go 1.18 and the introduction of generics, this is a good example for its usage to don't have to write (more or less) the same code again and again.

There is a more formal and longer definition, that you can check out. [2]

Overview

In this example we will create a generics repository and store some objects in these. We will use simple user and pet structs, without any complex internals. A user looks like this:

    type User struct {
    	id      uuid.UUID
    	Name    string
    	Hobbies []string
    }

The user must also implement the user interface, as we want to use the ID() method later in our generic repository.

    type User interface {
    	ID() uuid.UUID
    }

Introduction to generics

Generics look more or less like in any other language. We will cover generic structs (and methods), generic functions, their instantiation and usage. I also don't care about error handling in all these examples, I will simply panic if anything went wrong.

Generic structs

This is a generic struct of a repository:

    type GenericRepository[T storableConstraint] struct {
    	Store map[uuid.UUID]T
    }

The repository will store added objects in a map with a uuid.UUID key and a value T, which will be the actual type of the struct the repository will be created for.

A notable thing is the storableConstraint. A constraint can define methods that a struct or interface used to construct this generic repository must provide. In this case the storableConstraint looks like this:

    type storableConstraint interface {
    	ID() uuid.UUID
    }

This means everything I want to store in a generic repository must implement a ID() method that returns a uuid.UUID attribute.

Let's have a look on a method on this repository, e.g. the Add method.

    func (t *GenericRepository[T]) Add(item T) error {
    	_, ok := t.Store[item.ID()]
    	if ok {
    		return nil
    	}

    	t.Store[item.ID()] = item

    	return nil
    }

As our storableConstraint ensures that every object has a ID() method, we can us it to get the ID of this object. The ID will then be used as the unique identifier in our internal store.

Generic functions

Besides generic structs and methods on these structs, we can also create generic functions. So lets say we want to transform one object into another one, we provide object T as input, but want to get object U as return value.

Another new thing with Go 1.18 is the any keyword. any replaces interface{} if you don't have a type for something. In this case we use any as the constraint, which means that every interface or struct is allowed to be transformed. Which is naive to use in real world application, but will suffice for this example.

We can create a simple function that will look like this:

    func transform[T,U any](incoming T) (U, error) {
        ...
    }

For a single generic value, the type can mostly be inferred and doesn't need to be provided while calling the function. In this case we have to provide it, so the function call looks like this:

    pet, err := transform[User, Pet](user)
    if err != nil {
        panic(err)
    }

Instantiation

So let's instantiate our generic repository, create a user and add it to the repository. In this case we will instantiate the repository with the user interface instead of the user struct. Please mind that the NewUser() function does return a interfaces.User instead of a User, so we ensure that a User actually implements the interface.

	userRepository := NewGenericRepository[interfaces.User]()
	user1 := NewUser("Peter", []string{"1", "2"})

	err := userRepository.Add(user1)
	if err != nil {
		panic(err)
	}

	userFromRepo, err := userRepository.Get(user1.ID())
	if err != nil {
		panic(err)
	}

	fmt.Println(userFromRepo)

We then create a new user and add it to the created repository. Then the repository can be queried with the user id, and we get our earlier added user back.

As the repository is a generic we can simply add another type, that fulfills our earlier defined constraint.

	petRepository := NewGenericRepository[interfaces.Pet]()
    petOfPeter := NewPet("Peter's Cat", user1.ID())

	err = petRepository.Add(petOfPeter)
	if err != nil {
		panic(err)
	}

	petFromRepo, err := petRepository.Get(petOfPeter.ID())
	if err != nil {
		panic(err)
	}

	fmt.Println(petFromRepo)

So as you can see, this repository can be used to store any object that implements a ID() method. This is also very handy if you want to mock a repository used in tests.

The whole example can be found in my code repository [3].

Conclusion

To be honest I was a bit skeptic about the addition of generics to go, as I thought with existing and good integrated tooling for code generation go didn't need them. But I was clearly wrong! The generics implementation and usage feels very simple and therefore go-like, and I really enjoy using them. Especially when writing simple APIs or mocks for some tests, generics come in very handy and can save quite some time.

If you are more interested in the internals, I can recommend this excellent post from Vicent Marti. It will show how go implements generics, but also how generics can make your code run slower. [4]


  1. https://go.dev/blog/go1.18

  2. https://deviq.com/design-patterns/repository-pattern

  3. https://github.com/andresterba/code-for-blog

  4. https://planetscale.com/blog/generics-can-make-your-go-code-slower