Generics Not Included or Needed ( In This Situation )

August 7, 2017 ยท 3 minute read

Commonly people say that the main weakness of golang is the lack of generics. I used to agree with this, but as I have continued to work with go, I have found that most of the time there are suitable solutions, though not all of them are perfect.

One problem that I have been working on solving has to do with generic operations on data from the database. Initially I used code generation to come up with “Generic” code, but there was a lot of boilerplate, and I found that it could be simplified

The goal of the code below is to make GetChildren generic. It is a function that taking a parent it returns a subset of its children that meet a certain criteria.

package DataSearcher

// The package genny essentially allows us to do a find replace on our template that can be seen above, and then
// create copies of that template that have Parent and Children replaced with the types you like.
import "github.com/cheekybits/genny/generic"

// The goal of this package is to provide a generic GetChildren function.  Parent will eventually be replaced with
// with the struct that you want to use this generic on compile time. This is a construct of Genny.
type Parent generic.Type

// ParentDataGetter gets information about the parent. The GetChildren function will take a look at all the data in
// ParentData and decide if it should include a child or not.
type ParentDataGetter func(Parent) (ParentData, error)

// ParentChildrenGetter returns all the children of a parent.
type ParentChildrenGetter func(Parent) ([]Child, error)

// Similar to Parent but in this case Children.
type Children generic.Type
type ChildDataGetter func GetChildData(Child) (ChildData, error)

// this is the meat and potatoes of this package. Using the given data providers it returns the subset of children
// that meet a certain criteria.
func GetChildren(Parent, ParentDataGetter, ParentChildrenGetter, ChildDataGetter) ([]Children, error) { ... }

This was my first attempt and I didn’t like it very much. I felt that there was a lot of room for improvement. My next version changed drastically.

type DataProvider interface {
    GetChildren(Parent) ([]Child, error)
}

type Child interface {
    GetData() (ChildData, error)
}

type Parent interface {
    GetData() (ParentData, error)
}

func GetChildren(Parent, DataProvider) ([]Child, error) { ... }

// Here we have an implementation of our dataprovider. It uses a database but it doesn't really matter how we get the
// data.
type DataProviderA struct {
    parentARepo db.ParentARepository
    childARepo db.ChildARepository
}

// we now convert our slice of ChildA to a slice of Child. Because of go's type system we have to iterate over all
// the values and copy them into a new array of the interface.
func (d DataProviderA) GetChildren(Parent) ([]Child, error) {
    childrenA, _ := d.childRepo.AllForParent(parent)
    typedChildren := make([]Child, len(childrenA)

    for i, child := range childrenA {
        typedChildren[i] = child
    }

    return typedChildren
}

This works a lot better and this is the solution that I ended up going with. Though recently I had a couple more ideas to improve it. The following idea came out of studying the standard library and remembering how the sort package sorts slices.

interface ChildrenSlice {
    Get(int) Child
    Len() int
}

type []ChildA ChildrenASlice
func (c ChildrenSlice) Get(index int) Child {
    return c[index]
}

After experimenting with different solutions I have found that merely complaining that Go isn’t C# ( I used to do ) and wanting C# features in go is unfair to the language. After I have spent more time using it I think that it is fair to say that it has almost all I need to get the job done in a fairly straight forward manner once I adjust my thinking.