Go Pointers: Why I Use Interfaces (in Go)

Kent Rancourt
4 min readJan 9, 2019

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.

Level setting

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.

Enter Go…

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.

Go doesn’t have constructors!

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:

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:

A widget might be instantiated and used like so:

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:

And we can easily amend our main function to use this new constructor-like function:

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 instances of the Widget type using our constructor-like function.

Going private

For our first attempt at enforcing instantiation of new Widgets via our constructor-like function, we'll start by ensuring our user-defined type is no longer exported. In Go, the capitalization of a type, function, 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 ("package private," essentially). Our Widget type therefore becomes widget:

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 a bad practice and warrants 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 private.” i.e. Anything unexported is an implementation detail of that package and should be of no consequence 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.

Interfaces to the rescue!

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 Widgets 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 directly by consumers.

Wrap up

I hope I’ve adequately covered this little idiosyncrasy 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!

--

--

Kent Rancourt

Kent is a founding engineer at Akuity, working primarily with Kubernetes, Argo CD, and other open source projects.