Stupid simple go code “generation”

Patrick Kelly
4 min readNov 5, 2017

--

One of the most common complaints, if not the #1 complaint, about go is its lack of generics. Generics can be extremely powerful, and very helpful in reducing copy/paste/hack coding. Generics can also lead to very confusing/abstract code.

What go does offer to mitigate the headache of managing boilerplate code is go generate. Go’s code generator facility allows a programmer to write any program they like to generate any code they like. All that’s needed is to include some magic stuff in some .go file. It’s even possible to use bash to generate your code. :)

//go:generate $GOPATH/src/github.com/pdk/binoislist/make-binois-list.sh animal_list.go animal Animal AnimalsList Animal{}

As an exercise, I wrote a “generic” list package (github.com/pdk/binoislist), and a couple bash scripts that will translate it to handle other types. With this I can quickly and easily have an AnimalList and a UserList and WhateverTypeList all with the same methods and capabilities, but each with complete type safety.

It seems that often people want to create generators that are very intelligent, parsing the source template, and being quite smart about translating from original source to output. I opted for using the tools at hand: bash and sed .

sed -e "s/binoislist/$PKG_NAME/g;" \
-e "/const UnknownBinois/d;" \
-e "s/UnknownBinois/nil/g;" \
-e "s/UnknownBinois/Unknown$BASE_TYPE/g;" \
-e "s/BinoisList/$LIST_TYPE/g;" \
-e "s/BinoisPtr/\\*$BASE_TYPE/g;" \
-e "s/Binois/$BASE_TYPE/g;" \
< $GOPATH/src/github.com/pdk/binoislist/list.go >> "$OUTPUT_FILE"

I simply selected a distinct name to use in my source file so that I could reliably convert everything with a simple tool. With this approach, all the comments, as well as the actual code, are converted.

For example, this:

// BinoisTesterFunc is a function that returns true/false given a BinoisPtr. Used
// for finding or filtering with a BinoisList.
type BinoisTesterFunc func(g BinoisPtr) bool
// Filter returns a new BinoisList of items where f() is true.
func (list BinoisList) Filter(f BinoisTesterFunc) BinoisList {
var newList BinoisList
for _, item := range list {
if f(item) {
newList = append(newList, item)
}
}
return newList
}

becomes

// UserTesterFunc is a function that returns true/false given a *User. Used
// for finding or filtering with a UserList.
type UserTesterFunc func(g *User) bool
// Filter returns a new UserList of items where f() is true.
func (list UserList) Filter(f UserTesterFunc) UserList {
var newList UserList
for _, item := range list {
if f(item) {
newList = append(newList, item)
}
}
return newList
}

This list package is built directly atop go slices, and a lot of the package is just syntactic sugar for functionality that is already present with standard slices. There are several methods that provide functionality beyond what is directly available with slices: Map() , Filter() , Index() , LastIndex() . None or these are major earthshakers, but often it’s nice to have a single line of code instead of writing yet another loop.

activeUsers := users.Filter(IsActive)

Dynamic Type Checking

Several methods in this package ( Find() , Index() , LastIndex() , etc.) require that a .Equals method be defined on the target object. How can you find an object in a list, if you can’t tell if it’s a match? Go is strongly typed, but with it’s late-binding interfaces and type assertions, it’s possible to write code that can use methods if available, or “handle it” when methods are not defined.

type UserEqualizer interface {
Equals(*User) bool
}
func affirmUserEqualsImplemented(methodName string) {
var t *User
var x interface{} = t
_, ok := x.(UserEqualizer)
if !ok {
panic("implement method *User.Equals(*User) in order to use " + methodName)
}
}
...
affirmUserEqualsImplemented("UserList.LastIndex(item)")

This is an example of defining an interface that contains the method we want to check, then using a type assertion to discover if the target type implements the interface, i.e., if it has the method.

I’ve chosen to panic if the method is not present, which is the same result as if I had made the assertion without checking first, but with this approach the error message can be more meaningful/helpful than the standard panic. Returning an error when a desired method is missing is also an alternative.

The net result of this approach is that there are several methods included in binoislist that require there be a .Equals() defined, but they will not trigger compile errors if their dependencies are not (yet) present. As soon as the client code attempts to use one of these methods, a (hopefully) clear error will indicate what method needs to be added.

Aside from the checking, actually invoking the desired method is not too ugly:

func (thing *User) Equalizer() UserEqualizer {
var x interface{} = thing
return x.(UserEqualizer)
}
...
user1.Equalizer().Equals(user2)

Conclusion

I hope this demonstrates that it is possible to handle some cases in go where “generics” might be considered the right solution, with the tools already in the go toolbox.

Also, go’s type assertions can allow for more “dynamic” programming, with error handling, when desired, without losing the advantages of strict type checking.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Patrick Kelly
Patrick Kelly

Written by Patrick Kelly

Web/database engineer/gopher. Cycling, photos, yada, yada.

No responses yet

Write a response