DEV Community

I Putu Ariyasa
I Putu Ariyasa

Posted on

Careful with those exported interface in your internal package

Internal package was first introduced in Go 1.4 to provide limitation to where the exported identifiers can be used. Quoting from the release note:

For example, a package .../a/b/c/internal/d/e/f can be imported only by code in the directory tree rooted at .../a/b/c. It cannot be imported by code in .../a/b/g or in any other repository.

This feature is important especially for library maintainer since it can help reduce reasons for introducing breaking changes.

As such, it might be tempting to just move all our shared code—that we'd expect to only be used inside our project—into an internal package. That way, we don't have to worry whenever we have to change their signature as needed.

However, do have some thorough inspection when those shared code is actually an interface. Consider this interface defined in an internal package under root directory of our project.

// internal/hidden/hidden.go
package hidden

type Hidden interface {
    Hide()
}
Enter fullscreen mode Exit fullscreen mode

Any package inside our project are allowed to use this interface. Note that use here does not only mean referencing hidden.Hidden or invoking the Hide() method, but also making it as a part of exported identifiers, like so:

// pkg/public/public.go
package public

import "our/project/internal/hidden"

func Public(h hidden.Hidden) {
}
Enter fullscreen mode Exit fullscreen mode

Despite hidden being an internal package, user of our project can still invoke public.Public by defining an implementation that satisfy hidden.Hidden. For example:

// some go files in other project
package main

import "our/project/pkg/public"

type hide struct {}

func (hide) Hide() { fmt.Println("im hidding")  }

func main(){
   public.Public(hide{})
}
Enter fullscreen mode Exit fullscreen mode

All is well, until sometimes in the future when we need to change hidden.Hidden, be it adding another method or modifying the signature of Hide() method. For example:

// internal/hidden/hidden.go
package hidden

type Hidden interface {
    Hide()
    Seek()
}
Enter fullscreen mode Exit fullscreen mode

The problem is not about the modification, but the release version that we will use after doing so.

Being a programmer and at the same time a human being, it would be straightforward to conclude that the changes we introduce is not a breaking change, since the modification is made only to an internal package. Moreover in a scenario where the interface with its current-and-only implementation are written by ourself but pkg/public/public.go is written by someone else in our team, the impact of the changes will be almost invisible.

But, as we go with that reasoning and release the change as a patch release, the users of our library who updated their dependency will start to complain that their code won't compile. Why? because their implementation of hidden.Hidden interface no longer satisfy the new constraint that we introduces, despite not being a major release.

Conclusion

Avoid making exported interface of an internal package a part of API signature. It will no longer becomes internal to our project implicitly. If unavoidable, it's better to move it out of the internal package and explicitly make it as part of the public API.

Top comments (0)