Golang: Vendoring and Interface Confusion

February 12, 2017

The last few days I have been working on a small tool that enables you to easily run tests against your code hosted on Github on your own machines. The thing is, there are tons of tools and providers that allow you to do this, but they'll start costing money as soon as you want to use them on private repositories.

So, inspired by Gitlab's excellent gitlab-runner, I started working on octorunner, a tool that implements a few of the features gitlab-runner has, like running tests on private repositories in ephemeral docker containers, triggered by push events from Github. As a bonus this was a nice chance to do a project in Go.

Anyway, after getting to a point where octorunner actually did something useful, I wanted to add some tests to octorunner. This is where a bit of headache started. See, I use the Docker client API and they vendor (all?) their external dependencies.

I had not come across vendoring in Go before, but since Go 1.5 - although experimental - it supports vendoring. It was enabled by default in 1.6. What happens when importing external packages into your project is that the compiler will first search for a package in your project's vendor directory, only after that will it search in $GOPATH/src.

Because the Docker client library vendored the context package - among others - I ran into issues when I wanted to define an interface that the Docker client was an implementation of. The exact compilation error I ran into was as follows:

ImageExists: *client.Client does not implement ImageLister (wrong type for
ImageList method) have
ImageList("github.com/docker/docker/vendor/golang.org/x/net/context".Context,
types.ImageListOptions) ([]types.ImageSummary, error) want
ImageList("context".Context, types.ImageListOptions) ([]types.ImageSummary,
error)

So what's happening here exactly? Let's take a look at a bit of code that reproduces this exact error:

package main

import (
    "golang.org/x/net/context"
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/client"
)

type ImageLister interface {
    ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error)
}

func main() {
    cli, _ := client.NewEnvClient()
    ImageExists(cli)
}

func ImageExists(lister ImageLister) {
    return
}

In the above example we're defining an ImageLister interface containing a single method. The argument causing the issue is ctx context.Context. The client.Client struct returned by client.NewEnvClient() should implement the ImageLister interface. However, the Context interface that's being used as type in Client.ImageList(...) uses the Docker client library's vendored Context, which is imported from $GOPATH/src/github.com/docker/docker/vendor/golang.org/x/net/context.

Context in my program is being imported from $GOPATH/src/golang.org/x/net/context. In the Docker library it's being imported from the previously mentioned path. This causes the compiler to treat them as two different interfaces, with the error as a result.

The thing about this that confuse(s/d) me, is that in Go all interfaces are implemented implicitly. So I thought: every object that implements Docker's vendored version of Context, also implements the local Context, so this should work. I might be missing something here.. I'll need to explore this a bit further.

In the end I had to properly vendor the Docker client library, causing both the (vendored) Docker client library's, and my own golang.org/x/net/context imports to be imported from $GOPATH/src/github.com/boyvanduuren/octorunner/vendor/golang.org/x/net/context, resulting in them being exactly the same interface.

TL;DR: When using library L that vendors other libraries, and you want to create a common interface that uses an interface from one of those vendored libraries, you'll likely run into the same problems as I did, and you'll have to vendor library L in your project.