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()
}
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) {
}
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{})
}
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()
}
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)