Writing generic Go code

Introduction

To me Go feels like a combination between the staticaly typed languages I learnt in school (C++, Java) and the scripting languages I’ve learnt to use at work (Ruby, Javascript). It is a strictly typed compiled language, yet writting it feels more like writting Ruby than C++. However Go does not support generics (or template functions, etc.); there is no way to parameterise a method with a type. This more than anything else prevents the language from having the same sort of agility as a loosely-typed scripting language.

To illustrate how interfaces allow for generic Go code, I’ll use the example of my program beerapi that provides functions for implementing a RESTful web server. (See EveryREST for its usage.) The database layer is abstracted in the adapters package. This package provides an interface to any data persistance layer. By calling only methods on the interfaces defined in that pacakge, the api package allows the persistance layer to be subsitituted trvivally.

Go interfaces

In Go an interface is a set of methods. An object is said to implement an interface if it is able to handle of the interface’s methods. There is no need (or mechanism) to specify that a type is implementing an interface.

Consider the POST method, responsible for creating new records.

func Post(dbTable adapters.Table, response http.ResponseWriter, request *http.Request) {
	data, err := ioutil.ReadAll(request.Body)
	check(err)
	object, err := Unmarshal(data, dbTable.RecordName())
	check(err)
	record := dbTable.NewRecord()
	record.SetAttributes(object)
	err = record.Save()
	if err != nil {
		response.Write([]byte(err.Error()))
		response.WriteHeader(400)
	} else {
		write(response, record, dbTable)
	}
}

The Table adapter has methods to return its records name (as is expected in the JSON request) and to create a new record. The return type of NewRecord() is itself an interface oftype adapters.Model. The model interface provides methods to set attributes on the record, and to save it. The details of these implementations are left up to implementing type. For example if the data provided fails validation on the persistance later, it is the responsibilty of the Save() method to return an error. This calling API code need not know anything about the underlying logic.

One interface many implementations

I have written two implementations of the adapters package that demonstrate the power of interfaces. The first beerapi/db simply stores the data in memory, the second beerds stores the data in Google Cloud Datastore.

To demonstrate how this works consider my two implementations of the Save() method. The first from beerapi/db:

func (model *Model) Save() error {
	return model.table.Save(model)
}

This method simply assures that the record has an ID so that it may be retrieved later. However to persist to the Datastore is somewhat more involved:

func (model *Model) Save() error {
	key, err := datastore.Put(model.table.database.context, model.key, model.entity)
	if err != nil {
		return err
	}
	model.key = key
	return nil
}

Here the datastore.Put mehtod is called. The fields model.key and model.entity are specific to this implementation, as required by the datastore package. The caller, such as the Post method above, does not care what type the model is so long as it satisfies the interface. The only difference between using either of these two implementations in EveryREST is the declaration of the Db variable that is responsible for providing instances of adapters.Table to be used by requests.

Runtime type evaluation

Eventually you will have to write code that takes a different path depending on the type that is implementing the interface. This is especially true in the case of the empty interface: interface{}. The empty interface is Go’s solution to generic programming. Since any type satisfies the interface, it may be used to pass any value to a function. In beerapi a model’s attributes are of type map[string]interface{}. In most cases the responsiblity for handling the specific type is delegated to the json package. But in the case of the Datastore, proprties are not simply serialized as JSON, the must be added to the datastore.PropertyList object.

func addProperty(destination *datastore.PropertyList, name string, field interface{}) bool {
	switch field.(type) {
	case []interface{}:
		for _, elem := range field.([]interface{}) {
			*destination = append(*destination, datastore.Property{
				Name:     name,
				Value:    elem,
				NoIndex:  false,
				Multiple: true,
			})
		}
		return true
	default:
		*destination = append(*destination, datastore.Property{
			Name:     name,
			Value:    field,
			NoIndex:  false,
			Multiple: false,
		})
		return false
	}
}

This function uses a type switch to determine the type of field being added, in this case we are simply interested in whether or not it is an array. If the field is an array, it is type cast, and each element is added to the PropertyList, otherwise a single property is added. This syntax may be used to determine a interface value’s type amongst any number of options.

The fact that this is the single use of a type switch in the whole program is a testament to the power of generic programming in Go.