Tuesday, August 5, 2014

Maximizing the Use of Interfaces in Go

Nerd alert: I made a type that satisfies every almost every Go standard library interface, just 'cause. And I noticed some interesting things about the standard lib along the way...

But in case you're in a hurry, I'll cut to the chase: Go programmers, as a community, should seek to minimize the declarations of new interfaces and improve on and maximize the use of existing ones.

Favorite thing about Go: can you guess?

Interfaces are my favorite thing about Go. (I share Russ Cox's sentiment for them.) Concurrency is cool too, but I am of the opinion that implicitly-implemented-interfaces trump even the mighty go keyword. (Seems that Dave Cheney thinks so too.)

To illustrate why, consider the tens of thousands of functions in Go code across the world that use an interface value of type error in its signature. All the functions that use error can automatically be compatible with your brand new type the world has never even seen before simply by adding an Error() string method to it.

Isn't that cool!? Two packages can work with each other without having to know anything about each other: the types just happen to have methods defined on them which implement a common interface. This has huge implications for how we, as a community, design software.

It gets better. When declaring your own interface, you can take an existing interface and embed it into yours, like io.ReadWriter does:

type ReadWriter interface {

Now any types that implement io.ReadWriter also implement io.Reader and io.Writer automatically. It's a 3 for 1 deal. Wow!

My interface addiction

A while ago, I self-diagnosed myself with Compulsive Interface Implementation Disorder (CIID). I was writing custom error types, first for the martini-contrib/binding package, then again for the more general mholt/binding. I satisfied my craving by satisfying the error interface. Then suddenly I realized what I'd done: I'd just given new life to thousands of existing libraries. All the code which requires an error could now use my new type.

Of course this isn't the first time I've used interfaces in Go before, and most likely I've been under-utilizing them. There was something so liberating about making this new type compatible with nearly everything else out there.

A look at interfaces in the standard library

As addicts do, I sought more of a high. So I wrote a little program that finds all the interfaces from the Go 1.3 standard library. Turns out there are 114 standard library interfaces.

Here's a few things I noticed while glossing over them.

There's plenty of overlap.

I asked the folks on Gophers Slack why several interface declarations contained duplicate methods: same names, same signatures. I found the following interfaces (there are probably others) with duplicate method definitions:

   Close() error

ReadRune() (r rune, size int, err error)

UnreadRune() error

ReadByte() (c byte, err error)

Write(p []byte) (n int, err error)

Seek(offset int64, whence int) (int64, error)

String() string

I am curious if this duplication is intentional, because each of these methods need only be declared once. The other interfaces could then just embed that interface where the desired method is declared, like we saw with io.ReadWriter. (Note that by "duplication" I mean the method name & signature, not its actual implementation.)

Some participants in Gophers Slack suggested that the duplication was for stability: if an embedded interface was changed, it would probably break code that used the embedder interface. Consider this frailty when designing your own packages.

In a comment, Jan Mercl pointed out that embedding a type (say fmt.Stringer) requires that package to import fmt, even if it doesn't use it for anything else. This should be taken into consideration when embedding.

Also, using embedding implies a working relationship with the embedded type. Simply sharing the method signature is not necessarily enough reason to embed. Even if an interface you made for your own http package happens to have the same methods as another interface in some obscure package like syscall doesn't mean you should import syscall and embed the type.

Most standard lib interfaces are small.

I seldom saw interface declarations in the standard lib with more than 3 or 4 methods. This pattern is beneficial to us.

When two interfaces share a common method, it might be wise to pull that method declaration out into its own interface. Small interfaces are easy to implement and embed to make larger interfaces when needed.

Effective Go suggests that small interfaces should be common.

reflect.Type is a HUGE interface.

Speaking of keeping interfaces small, did you know that the interface reflect.Type has 28 exported methods? I suppose these are for implementing run-time reflection. Large interfaces are exceptionally time-consuming to implement, so avoid them when possible. If you do declare a large interface, take advantage of embedding when you can.

Some interfaces look like others.

The expvar.Var interface acts just like the fmt.Stringer interface because they both have exactly one method and its signature is the same: String() string. It might seem annoying that there are two different interfaces with the same definition.

Which one should you implement? Actually, you don't have to choose. In Go, implementing one means you have implemented the other, since they are the same by definition.

Even though these two interfaces seem redundant, they document code differently. Using expvar.Var makes it clear that you're working with an exported variable, not just some generic type that can stringify itself. In other words, this "redundant" interface is self-documenting, and it provides extra cognitive context. You might furrow a brow if you saw a function use fmt.Stringer in the expvar package in a place where it seems more natural to use a Var.

If you follow this pattern in your own projects, be sure it adds clarity to what your code is doing, and use godoc to help explain its purpose.

Some interfaces almost look like others.

In Go, it's so easy to implement interfaces that it can be accidental.

os.Signal caught my attention because of the comment in its definition:

type Signal interface {
        String() string
        Signal() // to distinguish from other Stringers

It looks like fmt.Stringer, except for that pesky Signal() method. This pattern is actually similar to the one above with expvar.Var, but is slightly more rigorous.

What is the purpose of Signal(), which in practice which does nothing? I did a little digging in the os and syscall packages. Lots of low-level stuff happens in there. For your type to work with the functions that use an os.Signal value, you need to deliberately assert that your type is, in fact, an os.Signal, not a mere fmt.Stringer.

It's a clever trick. Use it sparingly though, as it's seldom necessary to prevent others from accidentally implementing your interface.

Interface implementations are documented as such.

Nothing fancy here: just write a comment when you deliberately implement an interface, like the Go authors did with syscall.Signal. This makes it obvious where and how you can use the type.

Just for fun: a supermassive type

I implemented as many standard lib interfaces as I could on a single type. The Everything type implements 103 of the 114 Go standard library interfaces. You can use it in ~90% of the standard library functions that have an interface in the signature.

Interface power

Go programmers should use interfaces often. Design for them, and define them carefully.

We should be talking about specific interfaces as much as we talk about packages and projects. Announce interfaces that may be useful to others.

I hope that it becomes common for a list of interfaces implemented by a package to be promoted to the project's README. Implementing popular interfaces should be described as a feature!

As we find and use existing, exported interfaces, our libraries will become more interoperative and compatible at an exponential rate. (Incidentally, a way to search exported Go interfaces would be awesome. SourceGraph? Anyone?)

Go programmers, as a community, should try to minimize the declarations of new interfaces and maximize the use of existing ones.

No comments:

Post a Comment