I'll take pkg over internal

Most people who would bother with the matter at all would admit that the layout of Go projects is in a bad way. To pkg, or not to pkg, that is the question. To put your project’s libraries under a directory named pkg, or not?

The pkg convention started when, prior to Go 1, the Go team put the stdlib in $GOROOT/src/pkg. Later they deleted the pkg directory and moved the stdlib to $GOROOT/src. (Brad Fitzpatrick talked about the history on Twitter.) By that time, projects like Kubernetes, Docker, Etcd had adopted pkg in their own projects.

Go doesn’t have an official project layout so the choices popular projects make are as close as the community gets to having a standard. These projects are often the first projects that Go programmers learn from. A GitHub repo called golang-standards/project-layout with 11K stars recommends pkg. Most of the most popular Go projects use pkg. New Go programmers ask why I don’t use pkg. I don’t see pkg going away unless the Go team publishes a doc that recommends not to.

I didn’t use pkg. Three factors caused me to reevaluate:

  1. The widespread use of pkg that isn’t slowing down.
  2. The community using internal as a replacement for pkg.
  3. The inconsistencies in a project’s structure that doesn’t use pkg.

You use internal directories to make packages private. If you put a package inside an internal directory, then other packages can’t import it unless they share a common ancestor. Internal packages enable you to export code for reuse in your project while reducing your public API. Russ Cox, in his proposal for internal packages, used the standard library as an example of where you want them. In the standard library, you:

  • Have a large API that warrants limiting;
  • Need to share your helpers across packages; and
  • Can’t export helpers.

Most projects don’t share these properties.

Many projects using internal packages have a single internal directory that encompass all the project’s packages with a command to make their code executable. These developers use internal to organize their projects, reducing their public API is incidental. They use internal for the same reasons people use the pkg directory. But internal is official: it’s the only directory named in Go’s documentation and has special compiler treatment. And using internal avoids the stigma of pkg. When I began writing Go, I noticed two things that are relevant that I liked: there were many quality libraries and it was super easy to use them. Deprived by internal.

There are people who don’t use pkg directories and do use cmd and internal directories. They don’t want pkg in their import paths, which is the main issue with pkg. They call pkg empty: directories containing directories with Go code are empty, directories containing Go files are not empty. Less useful than the other empty directories—cmd and internal. We use cmd directories because compiled commands take their names from their source directories and we use internal directories for the compiler’s rules for importing internal packages and these are compiler idiosyncrasies.

The pkg people don’t use the pkg directory because of the compiler’s idiosyncrasies. The pkg people say the directory is useful boilerplate that clarifies the project’s layout for people. Useful boilerplate for clarity sounds like a Go tagline. In non-trivial projects, you have directories that don’t contain Go code—namely hack, docs, build, examples, vendor, scripts, website, assets. With pkg directories, developers know to find the project’s Go code in the cmd, internal, and pkg directories: developers know that the docs directory contains the documentation and not a docs Go package.

The cmd, internal, and pkg directories group packages. cmd groups command packages, internal groups internal packages, and pkg groups public packages. A pkg directory beside the cmd directory is more consistent than a cmd directory beside your regular packages.

You don’t want others to rely on code that may change. People want your code because it solves a problem they have. They still have the problem after you make your packages internal. They solve it by copying your code if it’s open source, or by reimplementing if not. The copy or reimplementation doesn’t depend on the original version, the relationship is in someone’s head and they lack the tools to handle when you make changes. If the codes implement a third-party’s spec, that’s OK but not ideal. When the original code is the spec, that’s a problem.

The Go team wrote the module mechanics in internal packages. “For the initial development of module support,” Russ Cox said, “we’ve kept everything in internal directories to make it easier to make changes as our understanding of what the pieces should look like becomes clearer.” Easier for the developers: who’ve reimplemented the mechanics already. Who’ve followed the Go team’s changes and will throw out their code when the Go team exports the mechanics. Who, along with the people depending on these reimplementations, will migrate to the exported packages. Easier for the Go team: who will update their own code for the export changes. This was easier than exporting a package at the beginning for people to depend on, with APIs shielding people from the internal changes, and with versions protecting them from breaking API changes.

Go popularized no-style programming. We all defer to gofmt. Rob Pike coined the proverb: “Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite,” Pike continued, “A lot of people say—especially beginners—I want to move the braces; why tabs instead of spaces? Or whatever. [And I say] Who cares? Shut up.” Go is not a there’s more than one way to do it language. There’s one way to format code, one way to write a loop, one way to increment a number. But before you code Go you must decide how to lay out your project. Most popular language communities have layout conventions: Rust, Python, Java, Ruby. Rust has rules that say: Name an executable’s main source file src/main.rs. Name a library’s main source file src/lib.rs. Put other executable files in src/bin/*.rs. Rust programmers think about project layouts like Go programmers think about code formatting—they don’t.

With that said: I’ve begun using pkg. Trying it out. pkg is consistent with cmd and internal and other non-package directories. My projects’ contributors know what directories contain Go packages and which don’t. I structure my projects without making all my packages internal. And I don’t need to explain why I don’t use pkg anymore.


I’m writing a book for PragProg called Building Distributed Services with Go. You can sign up on this mailing list and get updated when the book’s available.