Emphasis on I and in Go.
If you've been coding for a while, I probably don't need to explain all the obvious benefits of interfaces, but I'm going to take just a moment or two to level set before I dive into the more idiosyncratic reasons I use interfaces in Go.
Skip ahead if you're confidant in your understanding of interfaces in general.
Using interfaces-- collections of methods or behaviors-- in any language, really, creates a thin layer of abstraction between bits of functionality and consumers of that functionality. By coding to interfaces, calling code requires no awareness of the underlying implementation details of functions it invokes. This is extremely important because it promotes a clean separation of concerns among components.
There are a lot of neat things that you can achieve using interfaces that you could not otherwise. For instance, you can create multiple components that calling code can interact with in a uniform manner, even if the underlying implementation of those components varies wildly. This creates the possibility of swapping components that implement a common interface with one another at compile time or even dynamically at runtime.
A convenient real world example is that of Go's io.Reader
interface. All
implementations of the io.Reader
interface support a
Read(p []byte) (n int, err error)
function. Consumers coding to the
io.Reader
interface do not need to know where the bytes obtained by calling
that function come from.
All of this is common sense for anyone who has been programming for a while.
In Go, more so than in any other language I have worked with, I frequently find additional, less obvious reasons to use interfaces. Today, I'm going to cover one so ubiquitous that I encounter is multiple times in a typical day of coding.
Many languages you might be familiar with provide a language feature called constructors. Constructors permit the author of a user-defined type (in many OO languages, a class) to provide an officially sanctioned means of instantiating instances of their type with guarantees that any initialization logic that must be carried out has indeed been executed.
For example, imagine that all "widgets" must have an immutable, system-assigned identifier. In Java, for instance, this is easy to implement:
https://gist.github.com/436bea9bbb9dab22064ba5af91e209ec
Code that must instantiate a new Widget
can do so using the constructor:
https://gist.github.com/f607fdaf78432d7491fc6283bcdc8c5e
There is no way to instantiate a new Widget
without its initialization logic
being executed. Brilliant!
Go doesn't have this feature. :(
In go, instances of user-defined types are instantiated directly.
Given the following:
https://gist.github.com/c181650920eb7919cd867c3eb1345e1b
A widget
might be instantiated and used like so:
https://gist.github.com/d11b6396bc8a12bbcef2a177c569a746
If you run this example, the (perhaps) unsurprising result is that the identifier that is printed is the empty string-- because it was never initialized and an empty string is the "zero value" for a string.
We can add a "constructor-like" function to our widgets
package to handle
initialization:
https://gist.github.com/bb5c135377a8a1c3f36c5ad6cf51925a
And we can easily amend our main
function to use this new constructor-like
function:
https://gist.github.com/c2d215e561cb1b0dc97beeb13d620563
Executing this program, we find the desired result.
But we still have a HUGE problem! Absolutely nothing forces a user of our
widgets
package to instantiate instanes of the Widget
type using our
constructor-like function.
For our first attempt at enforcing instantiation of new Widget
s via our
constructor-like function, we'll start by ensuring our user-defined type is no
longer exported. In Go, the capitalization of types, functions, etc. determines
whether it is exported (accessible to other packages) or not. Names that are
capitalized are exported ("public," essentially) while names that are not
capitalized are unexported ("packae private," essentially). Our Widget
type
therefore becomes widet
:
https://gist.github.com/dbfa965a3f08604753bb4975ca45e575
Our main program remains unchanged and should still work as-is. This is a step
closer to what we want, but we've committed a non-obvious cardinal sin in the
process. The constructor-like NewWidget()
function returns an instance of an
unexported type. While the compiler does not balk at this, this is still
considered bad, but required some explanation.
In Go, packages are the fundamental unit of reuse. (Contrast this to other
languages where a class might be the fundamental unit of reuse.) Anything
unexported is, as previously noted, essentially "package privte." i.e. Anything
unexported is an implementation detail of the package that should be of no
consequene to other packages that consume ours. Owing to this, Go's
documentation-generating tool godoc
does not generate documentation for
unexported functions, types, etc.
By returning an instance of the unexported widget
type in our constructor-like
function, we inadvertently created a dead end in the documentation. A developer
invoking our constructor-like function may obtain an instance of a widget
, but
will never find any documentation covering the existence or proper use of the
ID()
accessor function. The Go community takes documentation very seriously,
so this is frowned upon.
Recapping what got us to this point, we worked around Go's lack of support for
constructors by crafting a constructor-like function, but to ensure consumers
utilize that function instead of instantiating Widget
s directly, we changed
the visibility of that type-- renaming it as widget
in the process. The
compiler allowed this, but it created an annoying dead end in the documentation.
Nevertheless, we're a step closer to where we want to be. Interfaces will take
us the rest of the way.
By creating an exported interface that our widget
type will implement, our
constructor-like function can return something that is exported and documented
instead of something that isn't while the underlying implementation of the
interface remains unexported and cannot be instantiated direclty by consumers.
https://gist.github.com/6afd7ff1b44bb60418c761e50f38baf3
I hope I've adequately covered this little idiosynchrasy of Go-- how the language's lack of constructors often becomes impetus for the use of interfaces where they might otherwise not be called for.
In my next post, I'll cover a scenario that is nearly the inverse of this one-- a case where you might have used an interface in any other language, but can get by without using one in Go!