Interface Upgrades in Go
November 5, 2014Much has been written about Go’s interfaces. They’re one of the language features I like best, providing a very pragmatic trade-off between the complexity of more powerful type systems and the anarchy of duck typing. If you’re not already familiar with Go or Go’s interfaces, I’d encourage you to read an introduction to Go and to play around with the language a bit—it’ll help you better understand the remainder of this post.
Interfaces in Go are most commonly used as a means of encapsulation. They allow programmers to hide an object’s underlying state, only exposing a carefully curated API. They also allow objects to be decoupled in a way that allows them to be easily replaced by alternate implementations or stubbed out entirely.
However, Go’s interfaces—and in particular, interface conversions—allow for
a more interesting and less obvious pattern that I call “interface upgrades.”
The crucial observation here is that not only can interfaces be safely cast to
narrower interfaces (i.e., every io.ReadCloser
is also an
io.Reader
), but they can also be cast to wider or even unrelated
interfaces if their dynamic types support it.1
The reason I call these sorts of casts interface upgrades is that they remind me of protocol upgrades, e.g., the one performed during HTTP to negotiate the use of web sockets. In both cases, two cooperating parties communicating over one specified protocol (HTTP in one case, and some interface type in the other) can attempt to switch to another protocol with a different set of features.
A good place to search for Go examples is its standard library, so in order to illustrate interface upgrades, let’s dive into three examples taken from the source of Go itself.
Efficient io
Like all articles about Go’s interfaces, we are obligated to start with Go’s
io
package.
The io
package is essentially a set of protocols for copying bytes around.
Unfortunately, this is quite slow, and therefore it’s in Go’s best interest to
try to move each byte as few times as possible.
There’s quite possibly no better place to attack this problem than
io.Copy
, Go’s powerhouse of byte moving. This function takes an
io.Reader
and an io.Writer
, and moves data from one to
the other. Simple enough.
But there’s a catch: both io.Reader
’s Read
and io.Writer
’s Write
take
buffers as arguments: they expect their caller to provide the necessary memory
for them. The only sensible implementation of io.Copy
, then, is to allocate a
buffer and pass it alternately to Read
and Write
until all the data has been
copied. The end result is that every byte gets copied twice: once when Read
places it into the Copy
-internal buffer, and once when Write
removes it.
In many cases we can do better, however. If we’re writing to a file from an
internal bytes.Buffer
, for instance, there’s no need to allocate an
intermediary buffer at all: we can pass bytes.Buffer
’s internal buffer to
syscall.Write
directly, turning a two-copy process into a single-copy
one.
Supporting this kind of copy elision in io.Copy
is tricky since we can’t
change the signature of either io.Reader.Read
or io.Writer.Write
without
sacrificing generality. Luckily, interface upgrades come to our rescue. The io
package defines two auxiliary types, io.WriterTo
and
io.ReaderFrom
which io.Reader
s and io.Writer
s (respectively)
may optionally implement. If at least one of the sides of any given io.Copy
can be upgraded to one of these alternate interfaces, they can arrange to copy
data directly between their respective buffers (or to use the other’s buffers
directly), eliminating the need for io.Copy
to allocate buffers of its own.
You’ll find that many of Go’s built-in buffer types (e.g., those in package
bufio
, as well as bytes.Buffer
and strings.Reader
)
also implement io.ReaderFrom
and io.WriterTo
, allowing many common io
pipelines to be performed with less byte movement than one might expect.
Extending net/http
Interface upgrades can also be used to add functionality to existing interfaces, especially when you are unable (or unwilling) to modify an existing interface.
A good example of this is net/http
’s
ResponseWriter
. In particular, net/http
exposes three
additional interfaces, CloseNotifier
, Flusher
,
and Hijacker
, each of which augment the capabilities of a vanilla
ResponseWriter
. The default implementation of a ResponseWriter
incidentally
supports all three of these additional interfaces (although nothing in the
exposed types would tell you that), and using type assertions to upgrade a
http.ResponseWriter
to one of these other interfaces allows you to unlock this
additional functionality.
For instance, if you wanted to flush an HTTP response body halfway through, you might write:
func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "It's going to be legen... ")
io.WriteString(w, "(wait for it) ")
if fl, ok := w.(http.Flusher); ok {
fl.Flush()
time.Sleep(1 * time.Second)
}
io.WriteString(w, "...dary. Legendary!\n")
}
The variable w
is a http.ResponseWriter
and does not ordinarily support the
Flush()
function, but by performing a type assertion to
http.Flusher
you can upgrade to a interface type that does.
For library authors, this sort of interface upgrade is pretty neat, since it
allows them to provide additional functionality in a backwards-compatible way.
By simply defining additional methods on some interface’s dynamic type (in the
case of net/http.ResponseWriter
, this is a private struct named
response
) you can let people opt in to new methods without
breaking any existing code.
For consumers of libraries, including standard ones like net/http
, the story
isn’t quite as great. While it just so happens that all the ResponseWriter
s
that net/http
give you implement (e.g.) CloseNotifier
, there’s not really
any way for you to know that without reading the source. The type system
certainly can’t tell you this (almost by design), and even the
documentation—which is normally very good—falls short here. This is
by no means inherent to interface upgrades themselves (compare
CloseNotifier
’s documentation to, for instance,
io.Copy
’s), but without a concious effort to document supported
upgrades they’re likely to remain largely unused.
This is also probably a good place to mention that interface upgrades put you
beyond the safety of the type checker. If you write an invalid upgrade either
due to programmer error or due to a change in a library you’re using, the way
you’ll find out is when your program panic
s at runtime. Therefore, you should
always use the “comma, ok” idiom when performing type assertions, and always
provide fallback behavior for when you discover that an upgrade cannot be
made.2
Optimizing net
A final, rather astounding example can be found in Go’s net
package.
As I mentioned above, moving data around is pretty slow, and a good way to make
programs faster is to avoid copies at all. Unfortunately, the normal read and
write cycle forces us to do at least two copies (regardless of whatever
io.Copy
tricks we do in Go): once from kernel space to receive data, and once
to kernel space to send it somewhere else.
Just like before, we can sometimes do better. Certain operating systems expose
system calls like sendfile(2)
(available in some form on most
UNIX systems; a similar TransmitFile
mechanism exists on Windows), which
instructs the kernel to move data to and from certain sorts of file descriptors
with a single kernel-internal copy operation, an improvement over the ordinary
two kernel-to-userspace copies.
Making use of these more efficient system calls often requires careful application-level bookkeeping and invasive architectural changes, and as a result only high-performance HTTP servers typically use them. Go, however, can simply leverage its existing abstractions, treating this as an interface upgrade like any other.3
Let’s dive in to the source of net
to have a look. net.TCPConn
,
the type underlying every TCP connection, defines a function which should now be
familiar to you: io.ReaderFrom
’s ReadFrom
. You’ll notice that
this function first attempts to call a function named sendFile
, falling back
to a generic ReadFrom
implementation if that didn’t work.
sendFile
is where all the interesting work happens. Since the
sendfile(2)
system call only works when sending regular files (and not, for
instance, a bytes.Buffer
or any other io.Reader
), sendFile
first attempts
two type assertions to determine if the io.Reader
it was passed happens to be
a os.File
, possibly wrapped in an io.LimitedReader
. If
so, the io.Reader
is (potentially) eligible, and sendfile(2)
is used to
shuttle bytes around.
Let’s stop here for a second and think about how cool this is. The Go standard
library has automatically, and in all likelihood without you knowing, upgraded
every io.Copy
from a file to a TCP socket to use a system call that was
previously reserved for high-performance proxies, just because it could.
But that’s not even the most amazing thing. By making this upgrade path rely on
the standard io.ReaderFrom
interface, Go has given us enough rope to allow
any io.Writer
that wraps a net.TCPConn
to take advantage of this
optimization, just by enabling an interface upgrade to io.ReaderFrom
.
So let’s tie this entire post together by going back and having another look at
http.ResponseWriter
. We previously looked at how it offered additional
functionality through interface upgrades. What we overlooked at the time was the
fact that the default http.ResponseWriter
actually implements one more method:
ReadFrom
.
So not only does net
perform an interface upgrade to support sendfile(2)
when it can, but net/http
is able to support the same upgrade to make every
io.Copy
to an http.ResponseWriter
—for instance, the one in the standard
http.ServeFile
function—also support sendfile(2)
. Because
of an unlikely combination of well-designed interface types and the ability to
upgrade to more efficient interfaces when necessary, Go is able to serve files
as efficiently as nginx without your knowledge or cooperation.
And that’s fucking amazing.
The Proxy Problem
Interface upgrades aren’t without their faults. Besides the aforementioned problems for library users, interface upgrades present an enormous burden on authors of proxy objects.
Let’s again look at http.ResponseWriter
. Let’s say we want to write a request
logger middleware that captures and prints out the HTTP status code that we
returned. A naive solution might look like this:
type StatusLogger struct {
http.ResponseWriter
StatusCode int
}
func (s *StatusLogger) WriteHeader(code int) {
s.StatusCode = code
s.ResponseWriter.WriteHeader(code)
}
func RequestLogger(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
sl := &StatusLogger{ResponseWriter: w}
h.ServeHTTP(sl, r)
log.Println("HTTP status code", sl.StatusCode)
}
return http.HandlerFunc(fn)
}
Let’s set aside all the ways in which the code above is subtly incorrect and
focus on the interaction between the StatusLogger
and the
http.ResponseWriter
it wraps. Our intent is fairly clear: we want to intercept
all calls to WriteHeader
, saving the status code that is sent, but otherwise
act exactly as the object we’re proxying.
Unfortunately, our StatusLogger
only implements the bare minimum required of
an http.ResponseWriter
, and in particular doesn’t implement any interface
upgrades. Users who are expecting upgradable functionality (including those that
rely on the performance characteristics of, e.g., sendfile(2)
upgrades) will
find their applications broken in subtle and in all likelihood, hard-to-debug
ways.
In general, it’s impossible to know which interface upgrades a given object
supports, since doing so would require deep knowledge of the method set of a
interface’s dynamic type. It’s therefore also impossible to write a
general-purpose proxy type that supports arbitrary interface upgrades. Even if
we have only a small number of interface upgrades to support (for example.
http.ResponseWriter
’s four), supporting all combinations of those interfaces
becomes an unwieldy power set (http.ResponseWriter
would require sixteen
implementations).
But in practice it’s possible to do pretty well, especially for standard library
types like http.ResponseWriter
s. Here, for any given version of Go we have a
single well-known set of upgrades to support. In this case, there are two common
configurations: a bare http.ResponseWriter
, and one with all the fancy bells
and whistles. If you want to see this technique at work, I’ve written a generic,
interface upgrade aware http.ResponseWriter
proxy as part of
Goji.
Use Sparingly
Interface upgrades allow for some of the coolest behaviors in Go, but I’m going to end this post on a somewhat unusual note: I’d strongly encourage you not to write your own.
Interface upgrades derive a lot of their utility from being standard. Just like
a network protocol upgrade, both sides need to agree on what the resulting
behavior is. There are a few well-known interface upgrades in Go’s standard
library which I will happily promote the use of (particularly the ones in io
),
but chances are your library isn’t standard enough to make good use of a custom
interface upgrade. The downsides of sidestepping the type system are very real,
and I doubt the complexity will often be worth it.
It took me a while to truly appreciate Go’s interfaces: they manage to hide a remarkable amount of complexity for such a simple idea. It’s really amazing how much power you can get from just a few well thought out interfaces and the means to convert between them, and it’s a constant reminder of how well designed Go’s standard library is.
-
I’m sure I’m not the first to notice this phenomenon, which means that someone else has probably already given it a different name. If you know of any interesting articles about this from the literature or elsewhere on the internet, please send them my way. ↩
-
If for whatever reason this is impossible in your application (for instance, you rely on being able to
Hijack()
ahttp.ResponseWriter
), you should be sure to have “pre-flight” runtime checks to ensure all the interface upgrades you rely on are supported. ↩ -
There’s actually a second system call available on Linux,
splice(2)
, which allows the efficient transfer between an arbitrary file descriptor and a pipe. By splicing one file descriptor to one end of a pipe and another file descriptor to the other end, you can use this system call to perform arbitrary fd-to-fd copies without ever involving userspace. Unlikesendfile(2)
, Go doesn’t currently support this using interface upgrades, but I can’t think of a reason it couldn’t. ↩