Introduction

Go programmers would know about the lack of generic in the language (even at 1.7 at the point of writing). Technically, Go has generic, append, make, … are some examples. It’s simply not accessibly to normal programmers. The website says that generics are convenient but come at a cost in complexity in the type system, and that it is possible to write code that does what generics would enable. In this article, I want to explore how to replace generics.

The goal of generic is to save us from implementing the same code over and over again. Consider the simplest example of sorting an array. Suppose you wrote a sorting algorithm for int, and now you want to use the same logic for sorting int64, float64, and potentially your own type t1. It would be mundane to copy and paste the code 4 times.

The standard library elegantly solves this by abstracting the elements to be sorted into an Item interface which has methods that are needed for the sorting algorithm to work.

That is one of the techniques that can circumvent the problem. There are some other techniques as well.

So far my top candidates are:

  • Interface and wrapper
  • Reflect
  • Copy and paste
  • Code gen

Interface and wrapper

All problems in computing can be solved by another layer of indirection

Just like the sorting example, we can have our functions for implementing the business logic take an interface instead. The interface would expose methods that are needed for the logic to work. To use the logic for different types, we would need to write wrappers around it.

If we look at the sort package, we have wrapper functions to wrap floats and ints into a struct that implements the interfaces which quick sort needs in order to work.

Reflect

Consider a CRUD application. You have a model, which is a representation of an entity in the domain we care about. For example, we create a person struct to model what being a person means. We want to be able create and retrieve the data from some database.

Suppose the signature of the function you have to work with is

func (c *dbClient) Retrieve(id *Key, dst interface{}) error {
    // puts the entity from the database into dst
}

func (a *modelAbstraction) Get() (*Model, error) {
    // 1) do instrumentation
    // 2) do logging
    // 3) do auth checking
    // 4) **create model struct**
    // 5) retrieve data using dbClient
    // 6) return the model
}

The issue here is we have to repeat ourselves for each model we have. When the only thing that changes in each of these model is really the model struct. And god forbid the day you need to add a new component, or change one of the components because you would need to search through your codebase to change everything that uses this patter.

So perhaps you want to write less code and be DRY! With generics, you would have a Get<T>, where T only affects part 5 in the function.

func GenericGet(id *Key, t interface{}) (interface{}, error) {
    // 1) do instrumentation
    // 2) do logging
    // 3) do auth checking
    
    // 4) **create model struct**
    t1 := reflect.TypeOf(t).Elem()
    dst := reflect.New(t1).Interface()
    // 5) retrieve data using dbClient
    err := dbClient.Get(id, dst)
    // 6) return the model
    return dst, err
}

With this approach, we only have one place that deals with the logic of getting an etry from the database. The trade-off is now we have to cast the result we get from the GenericGet into the type we expect.


type myModel struct {
    x int
}

func main() {
    // set up dbClient
    m, _ := GenericGet(someKey, new(myModel))
    realM := m.(*myModel)
}

A good example of this paradigm is in the heap package. For example, to make a priority queue.

Copy and paste

In medio stat Virtus

A get this a lot: “in Go, to reuse code is to copy and paste”. But if we don’t copy and paste code, we might end up with <leftpad.io>.

The key is in judicial moderation. Sometimes copy and pasting is simply enough.

Code gen

Sometimes if there’s just too much boilerplate, code gen can be the solution. It’s basically just an automated and structured way of doing copy and pasting.

In the CRUD example, suppose we have a template to generate CRUD abstractions.


//go:generate crud.go . -name myModel
struct myModel {
    x int
}

func main() {
    m, err := myModelData.Get(id)
    fmt.Printf("x is %d", m.x)
}

This approach makes it easy to extend the generated code too, and append extra useful methods to it.

An extra benefit is faster code because of fewer allocation that using reflect requires. Don’t take my word for it, benchmark it!

Conclusion

I have shown some ways in which we can get around the lack of generic in Go.