Golang: one function, one argument – multiple types

For better or for worse, Golang is an extremely simple language. It lacks some of the constructs other languages has. One of these features is generics (which at the time of writing, is about to be included in the language, though it hasn’t hit production yet). As a refresher, generics allow you to define variables whose type can be one of several. The title says is all really… there are situations where a developer would like to define just one function, since the task at hand would be the same across several different types. The function’s argument must be able to accept multiple types. There are plenty of tutorials out there expanding this point… some simple pseudo-code to illustrate this point would be:

variable DOG type ANIMAL
variable CAT type ANIMAL

function AddTail( any variable of TYPE ANIMAL ) {
   ... code to add tail ...
}

Golang currently lacks the usual tools we use to address the above situation like inheritance or generics. It leads to questions like:

How do you make a function accept multiple types?

func that can take multiple types

As the stackoverflow questions above indicate, the easiest way to tackle this in go is to simply pass in an argument of type:

interface{}

Interface{} is special because all types belong to it, so your function will accept any input type. However, any type is not the best… the compiler cannot help you check and validate your code.

While building CyberSift Tutela, we came across exactly such a scenario. A single function performing the same task on two different GORM structs. GORM structs represent different tables in your database. We had a task where a function would add the same kind of information to two different tables – leading to the scenario above. ideally we would have one function that could accept two types of structs (i.e. data from two tables). The most convenient way I found to address this is to use golang interfaces.

Let’s walk through a simplified version of the code. First, we define the required GORM structs to describe our tables (I’ll just call them job1 and job2):

type job1 struct {
	ID           int `gorm:"primaryKey"`
	UniqueField1 string
	CommonField1 string
}

type job2 struct {
	ID           int `gorm:"primaryKey"`
	UniqueField2 string
	CommonField2 string
}

Nothing special… enough to create a primary key… a unique field in each table to justify separate structs, and one common field which our upcoming function will update.

Next up some more boilerplate to setup our database connection, and create one row in each table:

var db, _ = gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

func main() {

	db.AutoMigrate(&job1{})
	db.AutoMigrate(&job2{})

	sampleJob1 := job1{
		UniqueField1: "foo",
		CommonField1: "old-common1",
	}

	sampleJob2 := job2{
		UniqueField2: "baz",
		CommonField2: "old-common2",
	}

	db.Model(&sampleJob1).FirstOrCreate(&sampleJob1)
	db.Model(&sampleJob2).FirstOrCreate(&sampleJob2)

	// worker(sampleJob1) //function to be defined shortly
	// worker(sampleJob2) //function to be defined shortly

}

Note the last lines currently commented out which are our worker function operating on two different types (type “job1” and type “job2”). A quick smoke test of the above code, opening the sqlite database in an editor should show you both tables, each having the appropriate data:

So far, so good. Tackling the “worker” function. A function is allowed to accept a variable which satisfies an interface. So let’s go ahead an define an interface which would allow us to update both tables:

type job interface {
	GetCommonFieldVal() string
	GetCommonFieldName() string
	GetDBModel() interface{}
}

The above interface, named “job”, requires our types to implement three methods. So as long as both our types implement all three methods, our worker function will be able to operate on either type.

func (j job1) GetCommonFieldVal() string {
	return j.CommonField1
}

func (j job2) GetCommonFieldVal() string {
	return j.CommonField2
}

func (j job1) GetCommonFieldName() string {
	return "common_field1"
}

func (j job2) GetCommonFieldName() string {
	return "common_field2"
}

func (j job1) GetDBModel() interface{} {
	return &j
}

func (j job2) GetDBModel() interface{} {
	return &j
}

That’s exactly what we do above. For each type job1 and job2, we ensure that the type implements all methods, and hence satisfies the “job” interface. That sets us up for a really simple “worker” function:

func worker(j job) {
	db.Model(
               j.GetDBModel()
        ).Update(
               j.GetCommonFieldName(), 
               "new-val"
        )
}

The worker function accepts an argument of type “job” – which is our interface, satisfied by two different types. Running our main function again and un-commenting the worker function, we see correct results in our database:

Takeaway

A golang function can have one argument which accepts multiple types of a restricted set, by writing said function to accept an argument which is a golang interface rather than a struct. Any struct implementing that interface will be allowed as input by your function.

Admittedly the exercise presented above has a lot of boilerplate written to satisfy the interface, however in complex projects this will allow your compiler to save you from a whole raft of bug classes.

Full code in this gist: https://gist.github.com/dvas0004/4d849c5212a65cf01c87d1c994ac5a75