When an Interface Depends on Another Interface in Go

Oren Rose
The Startup
Published in
6 min readAug 2, 2020
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.

Example

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.

Implementation

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

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

Ok great, so we have a type that 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 seem 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 a “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 of parameters.

Runner interface defines a method Run that 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 of constraint, we can use an anonymous type. The interface will then depend on a behavior instead on a specific type.

The first thing we can think of is just to 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 that 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 dependency 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.

Is this time to consider redesign?

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 about the design. There are cases in which we may over-engineer, and overuse interfaces and packages in our application. I had my fair share of using too many interfaces, thinking the more the merrier. In some cases, the correct solution was to redesign my code.

Just use import

After understanding the complexity, you may come to the 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 that 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.

Conclusion

Having an interface that depends on another one, is first a design issue. If the design is correct, this could mean you are developing a central framework that 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.

--

--

Oren Rose
The Startup

I’m a back-end developer, Golang enthusiastic and Clean Architecture zealous