Go Interfaces and the Handle/Body Idiom
The interface construct in the Go language is one of its most immediately visible features. Interfaces in Go are ubiquitous, but I am afraid that the best way to use them has not yet fully been explored. Moreover, in practice, Go interfaces seem to be used in ways that were not intended, and are not necessarily entirely beneficial, such as an implementation shortcut to the classic Handle/Body idiom that hides interchangeable implementations behind a common, well, interface.
Recap
Let’s recap. An interface defines a capability — something,
an instance of a type can do. For instance, the Namer
interface
represents the ability to return a name:
interface Namer {
Name() string
}
We can declare the type of (say) a function argument to be Namer
,
and the compiler will ensure that only types that implement a
Name() string
function will be permitted as arguments:
func f( n Namer ) {
fmt.Println( n.Name() )
// ...
}
What is interesting and unique is that the type implementing the
Name()
function does not need to reference the interface explicitly;
instead, the compiler figures this out. This is in contrast to, for
example, Java, where each class needs to be associated with the
interfaces it implements explicitly:
class Thing implements Namer {
...
}
As capabilities are added, it may become necessary to update a large number of classes with such explicit declarations — which may not even be possible, if the classes to be updated are part of an outside code base (such as a third-party library).
Polymorphism Without Type Hierarchy
Interfaces are Go’s way to allow for polymorphism. The function
f( n Namer )
above, for instance, does not care what type its
argument is: anything will work, as long as it provides a
Name() string
function. It would also be possible to collect
such items in a collection, and iterate over them (the most
directly explicit use of polymorphism, in my opinion):
ns := []Namer{ ... }
for _, n := range ns {
fmt.Println( n.Name() )
}
For anybody with a background in object-oriented programming, polymorphism suggests type hierarchies, but Go famously does not allow for that, and attempts to press interfaces into this role are likely to fail. It is tempting to see interfaces as supertypes and types implementing the interface as subtypes of this sypertype, but this kind of thinking leads astray, because the Go language does not provide the features to really make this work.
For example, one benefit of type hierarchies is that subtypes may inherit some functionality from the supertype and only implement the parts that are specific to each subtype themselves. But Go does not allow for that: not only may Go interfaces not contain variables (data members), but each type must implement the entire interface. One can try to get around this through embedding (for instance by embedding a base type with its implementations), but I have not found a way to do so that was worth the effort.
Go interfaces do not provide a method to enable type hierarchies by the backdoor.
Granularity
This discussion points to the question of the granularity of interfaces. Any talk of “supertypes” and “subtypes” suggests relatively large interfaces — in fact, it assumes types that represent an abstraction: a “Person”, or a “Business”, or a “RemoteClient”. But this is not what Go interfaces are intended for.
The Go documentation clearly envisions interfaces to be very fine-grained,
encompassing one or at most a few functions. Typical interfaces in the
standard library wrap a single Read()
or Write()
of Sort()
function.
This is a familiar notion, which in object-oriented modeling is sometimes
referred to as a “mixin” or a “trait”: a way to describe a capability of
an object, that is somewhat unrelated to the primary abstraction that the
type represents. Such mixins often have names that express the capability,
such as Serializable
, Sortable
, Stringable
.
Interfaces in the Real World
But the real world is not as neat and orderly as the Go Standard Library.
For instance, I am encountering a usage pattern that uses Go interfaces to implement the Handle/Body idiom from C++. Imagine a component in an enterprise application that you may want to stub out during testing, such as the database. This naturally leads to code like this:
type Backend interface {
Connect()
WriteCustomer( Customer )
ReadCustomer() Customer
DoThis()
DoThat()
// ...
}
func BackendFactory( mode string ) Backend {
if mode == "prod" {
return ProdBackend{} // the real thing
} else {
return TestBackend{} // a stub/mock
}
}
The factory function returns an interface, and depending on the mode (test or production), the actual type implementing this interface is either real or a stub.
I am deeply uncomfortable with this idiom, although I can’t
exactly explain why. Clearly, this usage is counter to the
intention of the Go interface feature (the Backend
above
is definitely not a mixin!), but that alone is not a reason
to reject an idiom: there is nothing wrong with finding new
and unanticipated applications for a language feature!
Maybe a bigger concern is that an abstraction mechanism is being used as an implementation shortcut. The interface, in this case, does not represent anything in particular, it is merely a wrapper (required by the static typing of the language) that forwards to the appropriate implementation. In C++, you would do the same thing, except that you would have to forward explicitly:
class Backend {
public:
virtual void DoThis() { impl->DoThis(); }
// ...
private:
BackendImpl *impl;
};
This is exactly what Go’s interfaces do under the hood: why should programmers write all this boilerplate themselves? The point is valid, and I don’t really have an answer.
What is true is that an explicit Handle/Body implementation (as in C++, but clearly also possible in Go), offers more flexibility. For instance, we can choose to have a single wrapper for multiple backend components (the “Facade” pattern). Or the wrapper can provide some business logic itself, by implementing some member functions itself.
On the other hand, Go’s embedding feature provides a bit
of flexibility not found elsewhere. For example, the Backend
interface itself could be a composite of smaller interfaces:
interface Backend {
CustomerBackend
ProductBackend
// ...
}
Testing can be conducted on the components, meaning that only a smaller set of functions has to be stubbed out for each component. This is a promising idea; unfortunately, decomposition by embedding is poorly understood (in contrast to decomposition by inheritance, for instance), and in my experience easily leads to more complexity, rather than less. (Do the components need to exist separately? What value does the combined type provide? The examples in the Go Standard Library do not tell a compelling story.)
So, I am undecided: I am uncomfortable using Go interfaces as shortcut for the Handle/Body idiom, but I am nevertheless glad that I don’t have to write the explicit forwarding boilerplate by hand!