Learning Interfaces Through Go

Confession: I’m new to interfaces, and this post is an attempt to grapple with them. As such, I’m even more open to comments and criticisms than usual.

Yes, They Really Are Like Contracts

I know I’m not the first person to argue this—and I must explicitly thank super genius Carl Hall of Cloudability for first introducing me to this insight—but interfaces really are a whole lot like contracts.

In most cases, you don’t care how a person fulfills their end of a contract. You just want to know what’s going to be delivered. If I order a Snuggie on Amazon, I don’t care if it’s delivered from down the street or Fort Worth, TX or the North Pole. I just want it in my mailbox. Or to put it in the words of Oleta Adams: “You can reach me by railway, you can reach me by trailway…I don’t care how you get here, just get here if you can.” I have my doubts that this epic 90’s ballad was talking about interfaces, but there is real wisdom there.

Interfaces allow you to say “I’m gonna give you a dodecahedron, and you’re gonna give me a surface area” or “I’m gonna give you an HTTP method and some other info, and you’re gonna give me a full-fledged HTTP request.” This is why it’s considered best practice to name Go interfaces with an “-er” at the end. They’re fulfillers of contractual obligations.

So why not just use functions for this kind of thing? Why not write, respectively, a calculateSurfaceArea function and a makeHTTPRequest function? Well, in many cases that is precisely what you should do. In the first example above, if all we care about is taking dodecahedrons and calculating their surface areas, then we should absolutely just write a function to do that.

But if we need something that can take any old polygon and return a surface area, then we’d probably be better off building a SurfaceAreaCalculator interface:

type Polygon struct {
    points int[]
}

type SurfaceAreaCalculator interface {
    SurfaceArea() float64
}

func (p Polygon) SurfaceArea() float64 {
    return …fancy math…
}

So now I’ve defined an interface that returns a SurfaceArea (as a float) and function that accomplishes precisely that. Now I can put that interface to work:

func main() {
    dodecahedron := Polygon{ ...points... }
    fmt.Println(dodecahedron.SurfaceArea())
}

The most important win here is conceptual. In my interface, I explicitly defined the problem I needed to solve. Now, I can produce Polygons of all kinds and simply ask for their SurfaceArea directly without using a function as a direct intermediary.

The Jump From Classes To Interfaces

I’m still new to Go and to procedural programming in general, having never been battle tested in C or Java or the like. For me, understanding interfaces couldn’t help but involve a direct comparison with objects and classes.

The difference between procedural and object-oriented classes is that classes are a different kind of contract. They’re not primarily a contract about what is being delivered but rather about what kinds of things an object is able to do. Let’s call it the “quack like a duck” contract (you may have heard of duck typing).

class Duck(object):
    def __init__(self):
        self.type = "duck"
    def looks(self):
        print "Looks like a %s" % self.type
    def quack(self):
        print "Quack!"

daffy = Duck()
daffy.looks()
daffy.quacks()

The last two method calls return “Looks like a duck” and “Quack!” respectively. It looks like a Duck and quacks like one, so we know what we’re dealing with here. The Duck class has fulfilled its contractual obligation because it’s making the kinds of objects we want.

It should be clear from this contrived example why procedural interfaces are different. Procedural interfaces are about return values and really only about return values. OO classes are about return values too, in a way, but classes also specify within their class definitions how return values are to be produced, using methods as an intermediary. Classes simply care in a way that interfaces don’t. In addition, getting return values out of a class requires instantiating a class (as I did above when I created daffy out of the Duck class). Go allows you to instantiate interfaces, but as in my example above it’s not strictly necessary.

Go Interfaces In Action: Packer

So I’ve established some essential differences between procedural interfaces and OO interfaces—errrr, classes—and I’ve used a hyper-basic snippet of Go code to do that. Now, let’s have a look at Go interfaces in the wild.

Packer is the latest brainchild of Mitchell Hashimoto of Vagrant fame. It’s a tool for creating identical machine images for Amazon EC2, VMWare, and a variety of other virtualization platforms. And it’s all done in Go. Its docs are absolutely stellar and it wouldn’t surprise me in the least if Packer ends up being as widely used as Vagrant.

One of the great things about Packer is that it’s extensible. If you want to run Packer on a not-yet-supported platform, you’re free to write your own plugins. For example, you can write a Builder that specifies how a machine is created and made ready to be provisioned.

All Builders are Go interfaces consisting of a Prepare, Run, and Cancel method:

type Builder interface {
    Prepare(...interface{}) error
    Run(ui Ui, hook Hook, cache Cache) (Artifact, error)
    Cancel()
}

Creating your own Builder simply means specifying what these three methods do (the other plugin types have their own methods). Packer structurally requires that Builders be able to do certain things, but Builders as interfaces leave open restricted horizons for later developers to fill in those structural gaps as they see fit. Packer knows that it’s going to have to function a certain way within any virtualization environment, but doesn’t deign to over-determine how that takes place.

Conclusion

If you’re already familiar with interfaces, very little of this should be new. But if you’re not, I think that Go is a great place to begin grappling with them. Go is a runtime that is unquestionably in ascension, and its emergence could lead to a more general rise in the importance of procedural languages at the expense of object-oriented languages. Hard to tell. But if that does come to pass, then understanding interfaces on an almost visceral level might be essential to getting by in a world of distributed systems, parallelism, concurrency, and the like—that is, the world that Go was explicitly built for.