Writing an interface that only sub-packages can implement

John Doak
3 min readMay 11, 2023

--

Sometimes we need to provide an interface that we only want our packages to implement.

The most common way of doing this is having a package that has a private method, so only types in that package can implement it.

type MyInterface interface {
SayHello()
internalOnly()
}

type Implementation struct {}
func (i Implementation) SayHello() {
fmt.Println("Hello")
}
func( i Implementation) internalOnly() {}

The code above provides MyInterface which has method internalOnly which can only be implemented by a type in that package. This is great and I often use this method to prevent others from implementing an interface that I expose.

Allowing others to implement an interface you define is dicey. I only do it when there is both a need and I KNOW that the interface won’t change. Otherwise adding a method at a later date will break all of my user’s code.

But this method doesn’t extend to handle the case where you want to organize your code with sub-packages. In that case, you might want to have a main package that defines an interface and sub-packages that implement that interface. But if you use the method above, the sub-packages cannot implement the interface. If you get rid of the private method, users can implement the interface and your interface becomes unchangeable without breaking user code (or doing a major version increment, which is breaking to the users in a different way). You can make a superset of the interface, but this clutters your API.

And while you can put all the types that implement an interface in the same package, I don’t find that as clean as using sub-packages.

A solution to the problem

Recently I came up with a solution that uses the internal/ package directory (I’ve never seen this method in use before, but it doesn’t mean someone hasn’t done it). For those that don’t know, internal/ can hold packages that can only be accessed by packages within a certain folder hierarchy. You can read about it here.

To achieve my goal, I create an internal package internal/local :

package local

type InternalInter interface {
internalOnly()
}

type InternalImpl struct {}
func(i InternalImpl) internalOnly(){}

Now this looks similar to what we do when we have a single package. InternalInter can only be implemented by a type in the local package. In this case we create the implementation InternalImpl .

Now, let’s say our package structure looks like the following:

hello/
hello.go
internal/
local/
local.go
sayit/
sayit.go

In the above structure, hello.go will hold our public interface. sayit.go will hold an implementation of the interface in hello.go .

hello.go will look like:

package hello

import ".../internal/local"

type SayHelloer interface {
SayHello()
local.InternalInter
}

Note: “…” stands for the package’s path.

So now the interface we have defined can only be implemented if they implement local.InternalInter we defined in our local package. No user packages can import local.

But our sub-packages can take advantage of this in our by doing the following:

package sayit

import ".../internal/local"

type SayIt struct {
local.InternalImpl // embeds the local.InternalImpl
}

func (s SayIt) SayHello() {
fmt.Println("Hello")
}

Now you have a sub-package that implements the interface in our package hello , but the public interface cannot be implemented by external packages.

Hopefully this method will help those of you trying to organize your code in a similar manner.

Good luck and happy coding!

Shameless plug

If you like this article and you are interested in using Go for DevOps work, check out my book:

Buy it on Amazon or any online book seller (I do not recommend the Kindle edition)

--

--

John Doak

Principal SWE@Microsoft, Ex-Google, Ex-Lucasfilm, Author of “Go For DevOps”, Photographer and general trouble maker