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.