When an Interface Depends on Another Interface in Go

Oren Rose
Oren Rose
Aug 2, 2020 · 5 min read
The small gopher is from the excellent free library by Maria Letta: https://github.com/MariaLetta/free-gophers-pack

Interfaces are the way for you to describe abstract behaviour, but what makes Go Interfaces so great? Well, it’s probably the first thing you learn about them, that they are implemented implicitly. You don’t need to explicitly specify about a type which interface it implements. This makes dependancy injection super easy in Go, leaving your code decoupled. You are defining the behaviour you need, and not what you do. Your type could implement more and more interfaces, without ever changing your code!

But when an interface depends on another one, you encounter a type dependancy, and not a behaviour one. In this post we’ll try to see why and how to handle this case.

When an interface is just a type.

So for a type to implement an interface, it must have all the interface’s methods. This means the methods’ names and signatures. Each method must take and return the exactly same type parameters. This strictness stays even if that parameter type is an interface.

Imagine we have an application for running jobs. We want to separate the implementation of the job and the way the job runs.

For doing the job we will define a Worker interface, and for running the job we will define a Runner interface. Then we will compose these two interfaces in a separated package called runner :

So the package runner doesn’t do much except exporting a function for running a Worker. It doesn’t know anything about what the job is and how does it run.

For the worker implementation we’ll have the following simple textworker:

Great, we have a type which implements runner.Worker. All we need to do for running runner.Run, is to supply a Runner interface. Meaning a type which implements Runner. A possible implementation for Runner would be to run the worker asynchronous. Like in this very deep and thoughtful example:

Ok great, so we have a type which implements runner.Runner interface, right? Well, as you already might have guessed, not exactly.

When trying to connect the dots in our main, we’ll get the harsh reality:

The line runner.Run(asyncRunner, worker) would cause us a compiler error. This is because asyncRunner doesn’t implement the interface runner.Runner.

At first look it might seems weird since async.Runner has this method:

Run(w Worker)

And runner.Runner is:

type Runner interface {
Run(w Worker)
}

If we would use an IDE, we would see a red line under asyncRunner. When hovering over that line, we would see “not so explanatory” error message:

Weird compile error message in IDE

Ok, what? Has our IDE gone crazy? What is the meaning of this?

The problem, as you guessed it, is that these interfaces are defined at different packages. Even if they describe the exactly same abstract behaviour, they are not the same.

When trying to build this in the terminal, we would see much more explanatory error message:

Well, now it’s way more clear. These are different interfaces. And for implementing an interface, a type must have the exact same method name and signature. That is, the method must use exactly the same type parameters.

Runner interface defines a method Run which depends on runner.Worker interface. And so it goes that any type who wishes to implement this interface must implement the method:

Run(w runner.Worker)

Well, this kinda ruins all the point of using interfaces, doesn’t it? We used an interface Worker in Runner interface so we would not be bound to a specific type. Don’t worry, like always, there’s a workaround.

When we don’t want to use types.

When an interface depends on another one, the compiler would insist on passing the exact same type parameter. Exactly like it does for any method in any interface. If we don’t want this type constraint, we can use an anonymous type. The interface will then depend on a behaviour instead on a specific type.

First thing we can think of, is just use an anonymous function instead of an interface:

If we still want an interface and not a function, we can go with an anonymous interface:

We could “improve” the last example a bit, by using an anonymous interface which embeds our Worker:

I know what you may be saying to yourself. “An interface in an interface in another interface? That just sounds like a dependancy with extra steps”

Need to remember that if we will use an anonymous interface in runner package, we’ll have to take the same approach on the implementor side.

For example, we can write our runner like this:

runner package that defines `Runner` interface with anonymous interface.

In this case our async.Runner will have to use an anonymous interface as well for implementing runner.Runner.

For example, async.Runner will have to look like this:

async.Runner implements runner.Runner by also using an anonymous interface.

So even though the async package doesn’t import runner, we are still bound to use an anonymous interface on both sides.

Compromises

In some (if not most) of these cases, using an anonymous type could look weird. This may be something we would like to avoid, although it is checked at compile time. When you read code like this, you will stop and ask yourself what was the intention of the writer by doing this:

Run(w interface{Worker})

On the other hand, even though I couldn’t find much examples showing this use case (one is available in this gist), maybe it’s a common and trivial practice I’m not very familiar with. Still, if the dependent interface signature is longer, there could be a lot of boilerplate to declare this interface every time.

Ok, so also “fixing” the issue with anonymous interfaces has its drawbacks. It should go without saying, that this complication should stay in your code only after thinking on the design. There are cases which we may over engineer, and overuse interfaces and packages in our application. I had my fair share with using too much interfaces, thinking the more the merrier. In some cases, the correct solution was to redesign my code.

After understanding the complexity, you may come to a conclusion that the package which defines the interface is a core package in your ecosystem. Only a few adapters components need to know about it. In this case you can argue that it should be imported in packages which wish to implement it. These few packages should be put in specific locations, in the outer layers of the application. Like an http middleware component may import http, for implementing http.Handler.

Having an interface which depends on another one, is first a design issue. If the design is correct, this could mean you are developing a central framework which could be imported by middleware.

In other cases it’s good to know about the solution of anonymous interfaces, although need to take into account it might not be the best practice.

The Startup

Get smarter at building your thing. Join The Startup’s +800K followers.

Oren Rose

Written by

Oren Rose

I’m a senior back-end developer at MinuteMedia, Golang enthusiastic and clean architecture fanboy.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +800K followers.

Oren Rose

Written by

Oren Rose

I’m a senior back-end developer at MinuteMedia, Golang enthusiastic and clean architecture fanboy.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +800K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store