Generate implementations for generic types in Go

⭐️ 👀 2023 WEB DEVELOPMENT BOOTCAMP starting in days! Join the waiting list to reserve your spot in my 10-weeks cohort course and learn the fundamentals, HTML, CSS, JS, Tailwind, React, Next.js and much much more! 👀 ⭐️

Go has strong, static types and it does not support generics, so how can we define general purpose data structures and algorithms that can be applied to more than one type? interface{}s are not the solution, as they require casting and we lose a lot of the advantages of having strong, static typing. The solution is code generation, as it allows to get compile-time checks and safety, and higher performance.

I didn’t find this information easily accessible when searching for code generation topics, but I stumbled on more complex use cases and scenarios, so here it is, explained in plain english.

Problem:

I want to implement a data structure (the same applies to an algorithm) in the most general possible way with Go, and generate type-specific implementations that I can easily reuse.

Solution:

Using genny, this couldn’t be simpler:

  1. import genny/generic
  2. define one or more types as generic.Type, e.g. calling them Item or Type
  3. use these types in your code
  4. run genny to generate the type-specific implementation

Example

Check this simple example, which is a trimmed down version of a Set Data Structure implementation in Go:

// Package set creates a ItemSet data structure for the Item type
package set

import "github.com/cheekybits/genny/generic"

// Item the type of the Set
type Item generic.Type

// ItemSet the set of Items
type ItemSet struct {
	items map[Item]bool
}

// Add adds a new element to the Set. Returns the Set.
func (s *ItemSet) Add(t Item) ItemSet {
	if s.items == nil {
		s.items = make(map[Item]bool)
	}
	_, ok := s.items[t]
	if !ok {
		s.items[t] = true
	}
    return *s
}

// Clear removes all elements from the Set
func (s *ItemSet) Clear() {
	(*s).items = make(map[Item]bool)
}

By running

genny -in set.go -out gen-set.go gen "Item=string,int"

in the command line now, if the file is called set.go it will generate a gen-set.go which contains the following:

// This file was automatically generated by genny.
// Any changes will be lost if this file is regenerated.
// see https://github.com/cheekybits/genny

// Package Set creates a StringSet data structure for the string type
package set

// StringSet the set of Strings
type StringSet struct {
	items map[string]bool
}

// Add adds a new element to the Set. Returns the Set.
func (s *StringSet) Add(t string) StringSet {
	s.items[t] = true
	return *s
}

// Clear removes all elements from the Set
func (s *StringSet) Clear() {
	(*s).items = make(map[string]bool)
}

// Delete removes the string from the Set and returns Has(string)
func (s *StringSet) Delete(item string) bool {
	ret := (*s).Has(item)
	if ret {
		delete((*s).items, item)
	}
	return ret
}

// Has returns true if the Set contains the string
func (s *StringSet) Has(item string) bool {
	return (*s).items[item]
}

// Strings returns the string(s) stored
func (s *StringSet) Strings() []string {
	items := []string{}
	for i := range s.items {
		items = append(items, i)
	}
	return items
}

// Package Set creates a IntSet data structure for the int type

// IntSet the set of Ints
type IntSet struct {
	items map[int]bool
}

// Add adds a new element to the Set. Returns the Set.
func (s *IntSet) Add(t int) IntSet {
	s.items[t] = true
	return *s
}

// Clear removes all elements from the Set
func (s *IntSet) Clear() {
	(*s).items = make(map[int]bool)
}

// Delete removes the int from the Set and returns Has(int)
func (s *IntSet) Delete(item int) bool {
	ret := (*s).Has(item)
	if ret {
		delete((*s).items, item)
	}
	return ret
}

// Has returns true if the Set contains the int
func (s *IntSet) Has(item int) bool {
	return (*s).items[item]
}

// Ints returns the int(s) stored
func (s *IntSet) Ints() []int {
	items := []int{}
	for i := range s.items {
		items = append(items, i)
	}
	return items
}

As you can see, genny has created the StringSet and IntSet structs from ItemSet**, because told it Item=string,int.

It also created the struct methods that compose our Set implementations.

If we want to add another set of types to our application we can edit the command and run it again.

Use go generate

You can also automate this by adding a comment on top of the file, below the package doc, like:

// go:generate genny -in=$GOFILE -out=gen-$GOFILE gen "Item=string,int"

This tells go generate, through the help of genny, to generate a version translating Item to string, and a second version translating item to int.

Run

go generate

to get the gen-set.go file, with string and int sets.

Testing

Important: the generic implementation of the data structure above compiles and can be tested as easily as a concrete implementation generated by go generate, so we can run tests against our generic implementation, as well as the type-specific ones.

One more thing! ⚠️ ✋

At the end of January I will organize the Web Development Bootcamp.

It's a 10-weeks long cohort online course where I will guide you to becoming a Web Developer.

It's not "just a course". It's a big event I organize once a year.

We'll start from zero, learn the fundamentals of Web Development, HTML CSS, JavaScript, Tailwind, Git, using the command line, VS Code, GitHub, Node.js, we'll then learn React, JSX, how to use PostgreSQL, Astro, Next.js, Prisma, deploying on Netlify/DigitalOcean/Fly/Vercel and much more! 

At the end of the first 10 weeks you'll know how to create web sites and web applications and I'll unlock you the 2nd phase of the Bootcamp: you will get access to a large number of projects exclusive to the Bootcamp graduates, so you can follow my instructions to build things like private areas with authentication, clones of popular sites like Twitter YouTube Reddit, create e-commerce sites, and much much more.

Because once you got the fundamentals, you only learn by working on real, exciting projects.

To find out more, visit bootcamp.dev