Who is Responsible for Writing Go Interfaces, Anyway?
For every function, data type, or other such code artifact, there are two bits of code: the part that defines and implements it, and the one that uses it.
As I have been thinking (here and here) about the proper use and understanding of Go’s interface construct, the question came up, who is actually responsible for writing the interface: the person who defines the data type, or the person who uses it?
The immediate assumption, of course, is that whoever defines a data type also provides the interface for it — in fact, one might want to start with the interface and provide its implementation(s) after. This is the notion behind an Abstract Data Type or a Facade: the interface publishes what the data type or module can do. It describes the “fan-out”.
But it is also possible to look at interfaces in the opposite way: not as a description of everything a data type can do, but as a constraint on what it minimally must be able to do. A function with the signature:
func f( w io.Writer ) { ... }
says that it will only use the Write(p []byte) (n int, err error)
function on its argument, nothing else. Used in this way, the
interface describes not the “fan-out” of a data type, but instead
the “fan-in” of the function f()
.
Thinking this through leads to an alternative mode of programming, where instead of starting with data types and their capabilities, one starts with functions and their input requirements. The person authoring the function(s) determines what common, minimal capability is required, and then writes an interface to express this. Bottom-up instead of top-down, in a way.
This is certainly how interfaces seem to be used in the Go standard
library. It also fits with the minimal interfaces commonly defined
there: io.Writer
, io.Reader
, and so on. It also provides a
natural application of Go’s “embedding” feature: as a way to combine
minimal interfaces as needed, while still maintaining the individual
interfaces as well. (Remember that a data type has to implement
all of an interface to fulfill it, so there is a need to keep
interfaces small, if they are to be widely applicable. It won’t
do to have just an io.ReadWriter
, because not everything will
implement both.)
This discussion does raise the question, though, what Go interfaces
then are, semantically. They are not really an abstraction mechanism:
abstraction is always “top-down”. Moreover, looking over some of the
typical interfaces defined by the standard library, it is noticeable
that they do not represent tremendous functionality: io.Reader
,
io.Writer
, io.Closer
, fmt.Stringer
, etc do not pack tremendous
semantic power!
This leads to the conclusion that Go’s interfaces are not a primarily semantic device, but mostly a technical convenience to get around Go’s static type system. The author of a function (and, to a lesser degree, of a struct) can make their code more generic by specifying a minimal interface that users of the code have to fulfill. It is then the responsibility of the code users to create their own data structures to fulfill the provided interfaces — in an almost complete reversal of the typical abstraction process that starts with the data types.
This certainly bears out my own experience: I have found it impractical
to express true semantic polymorphism (as you have with GUI elements, or
when writing game and simulation objects) using Go interfaces. Frequently,
I have found myself throwing the interfaces away, and implementing my
own dynamic method dispatch, using switch
statements or maps.
One last question: does this style of polymorphism lead to more downcasts (“type assertions”)? I have always viewed downcasts as an anti-pattern: as a failure of polymorphism and an indicator for improper modeling. But the polymorphism captured by Go interfaces does not represent semantic relationships among types anyway, hence this bias against downcasts may be inappropriate in this environment.