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 simply 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.

Want to hire me?

I'm currently considering remote job opportunities.

I'm interested in Go and/or JavaScript senior software engineer positions.

Read more about me and if you're interested, get in touch.