Skip to content

Instantly share code, notes, and snippets.

@krancour

krancour/blog.md Secret

Created January 9, 2019 20:51
Show Gist options
  • Save krancour/36438e5dcc9db19a767edebf7e14ace7 to your computer and use it in GitHub Desktop.
Save krancour/36438e5dcc9db19a767edebf7e14ace7 to your computer and use it in GitHub Desktop.

Go Pointers: Why I Use Interfaces (in Go)

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:

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.

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 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.

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 direclty by consumers.

https://gist.github.com/6afd7ff1b44bb60418c761e50f38baf3

Wrap up

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment