Thoughts on Whatnot
A blog about .
The Problem with Interfaces

In a recent talk, Russ Cox asked Go developers to write about problems they’ve run into with Go in an attempt to help steer the design process for Go 2. In this post, I would like to attempt to do so by explaining some of the issues I have with Go’s interfaces. To start with, I think it’s best if I explain the problem I think that Go’s interfaces solve before explaining where I think they fail. I will attempt to use actual examples where I can, but some of the problems that I will get into are difficult to illustrate with examples, as they have more to do with useful patterns that can’t be used rather than complete inability to do something.

Summary

To save some time, here’s a very brief overview of the main points:

  • Types serve the double purpose of distinguishing both data structure and functionality.
  • Go attempts to separate functionality via interfaces, but the system doesn’t do enough, leading to impossible abstractions in many situations.
  • Type systems are important in three locations: The declaration of types, the declaration of variables, and the assignment of variables.
  • Go’s interfaces put the burden of proper API design on the declaration of variables, a massive improvement over Java’s interfaces, which put the burden on the declaration of types.
  • Go’s interfaces, despite this improvement, still have a number of issues that result in a bad combination of reliance on library authors to think of the client’s use-case in advance while also making taking those use-cases into account tedious and error-prone.

A (Very) Brief History

The problem that Go interfaces were designed to solve stems from a design choice that far predates Go: Type systems. Computers have no concept of ‘types’ as modern programming knows them, a fact that began to cause issues as programs and programming languages grew ever more complex. Types stems were introduced to allow for compile-time checking for correct usage of structured data. However, after a while, a new problem arose. Specifically, when it became possible to attach functionality to types via, for example, methods, the structure of the actual data became less important, but type systems continued to care primarily about data structure.

As this discrepancy became apparent, various methods of dealing with it were introduced, usually piggy-backed onto the existing type system. Probably the simplest is simply inheritance-based polymorphism, which makes the often wrong assumption that types with similar functionality will also have similar structure. Go’s approach, presumably based on Java’s, is interfaces, and is my personal favorite solution most of the time, but it does have a number of issues, which I will detail below. Many languages eventually decided to introduce what is now known as ‘generics’, with varying degrees of success.

Go’s approach’s primary innovation was it’s inversion of the design responsibility from, say, Java’s interface system. Essentially, there are three main points in the flow of design where a type system is used in a C-style type system:

  1. Types are defined.
  2. Variables that will be assigned to later, possibly by someone else, are defined. This is most commonly function parameters.
  3. Variables are assigned to. This includes setting parameters by calling functions.

The power of each approach to dealing with type functionality can be analyzed by the number of these steps that it can be manipulated during. For example, Java’s interfaces are centered around the first step. This is a problem as it causes tight coupling between the definition of a type and it’s usage, which means that it’s harder to use types in ways the original author didn’t forsee.

Go’s interfaces, however, shift their primary point down to the second step simply by removing the requirement that an interface be explicitly implemented by a type. As a result, the thought process gets inverted, leading to interfaces not being a definition of a type’s functionality, but rather a definition of a variable’s functionality. This allows function authors to essentially request functionality for their parameters instead of structure.

Finally, type parameters, the most common form of generics, push this all the way down to the third step by allowing the caller of a function to specify specific types that fulfill the functionality requested by the function’s definition. Go, in contrast, doesn’t allow any control at this level at compile-time, but allows some at runtime in at least some cases in the form of type assertions.

Go’s Problems

Go’s approach, while clean and simple overall, is not without its limitations. Sometimes these limitations are simply annoyances, while other times they are much larger issues. In my experience, the actual issue issues break down into three main problems:

  • Lack of interoperability between interfaces and built-in types.
  • Lack of compile-time ability to track interfaced concrete types.
  • Lack of reflexivity.

The first issue is somewhat in between an annoyance and an actual issue. The problem can be illustrated by an attempt to write a reusable min function:

func min(v1, v2 Lesser) Lesser {
	if v1.Less(v2) {
		return v1
	}
	return v2
}

There are a number of issues with this, but the first is that this function doesn’t work out of the box with the types that it would likely be the most useful for: Numbers. Built-in types don’t satisfy any interface other than interface{}, so calling min(3, 5) is a compile-time error. This can be gotten around one of two ways: Either define a whole series of func min(v1, v2 int), func min(v1, v2 float64), etc. functions, or force the user to manually create a new type that wraps the built-in types and implements Lesser every time they want to use the function.

The first of these solutions is not tenable for two reasons. Firstly, it decreases code resuse, forcing reimplementation of the function for every type the author wants to bother supporting. For something like min(), that’s not really that big a deal, but more complex functions quickly become tedious and error-prone. Try implementing a binary tree for every one of the built-in types and let me know how it goes. Oh, and good luck supporting user-defined types. Secondly, this solution breaks the above stipulation on point of use by tying the functionality of the implementation to the data type, forcing users of the function to either stick with what the original author supported or find a way to convert, possibly with loss of precision and a bit of runtime overhead, to whatever the author did support.

The second solution makes the whole system more unwieldy for the client. A good example of this would be the sort package, where reimplementing slice wrappers for sorting things became so tedious that a new set of sort.Slice functions were introduced purely so that sorting, well, slices, would be less tedious and error-prone. The existence of that new API is a whole mini-argument in favor of looking into the whole problem.

The above example also illustrates the second issue, although to a lesser degree. Specifically, what type does the function return? This becomes more problematic very quickly with more complex implementations, such as for a linked list. The standard library’s linked list just uses interface{} for everything, but this loses compile-time type safety and incurs a runtime cost. In my opinion, this is probably the largest issue that interfaces have, as it comes up in almost every single code base.

The third problem is slightly harder to explain, and is rarely as much of an issue. However, it could become a lot more of an issue if certain solutions to the other two are implemented. Essentially, the problem comes from the fact that interfaces have no concept of the type they contain at the time of their definition. For example, let’s say that the first issue is solved by introducing automatic interface satisfaction for operations. Something along the lines of the following:

a := 3
var i Adder = a
// Valid by this solution. Essentially, make operators pretend to be
// methods when used with certain interfaces.

There’s a major problem with the above: What’s the definition of Adder? The seemingly most obvious interface { Add(Adder) Adder } will, as anyone who has even moderate experience with Go will notice, not work.

Conclusion

The bottom line is, essentially, that Go’s interfaces, while an improvement over Java and C#’s interfaces, don’t go far enough. Problems arise, regularly, while attempting to design reusable APIs. Data structure abstraction in particular is notorious for being completely impossible without both sacrificing compile-time type safety and incurring a runtime cost, not to mention introducing far less readable and more error-prone code.

Generics, specifically type parameters, are the standard solution to these issues. Though it may not be the best solution, examples of the need for a solution are so rampant that they can even be found throughout the standard library, and particularly in the various container subpackages, and most recently in the form of sync.Map. Just the fact that there are so few packages under container could be considered an example of these problems.

I look forward to seeing what solution Go comes up with, but come up with one it must.

comments powered by Disqus