What does putting a Go package under a directory named internal/ do, and who can still import it?
answer
- a directory name the build understands
- the parent directory decides the fence
- the tree rooted above internal/
- outside that tree it will not build
- the go command refuses, not a linter
basics
~20 sA package whose import path contains an internal element can be imported only by code inside the tree rooted at that internal/ directory's parent. Any other import fails at build time with 'use of internal package ... not allowed'.
solid answer
~50 sGo's toolchain enforces one directory-based import rule: if `internal` appears as an element of an import path, only code in the tree rooted at the parent of that `internal/` directory may import it. So `github.com/acme/svc/internal/store` is importable from anywhere under `github.com/acme/svc` — `cmd/api`, `pkg/client`, a test file — and from nowhere else. Another repository that imports it fails to build with `use of internal package github.com/acme/svc/internal/store not allowed`. This is not a convention or a lint: the go command's package loader refuses it, so `go build`, `go test` and `go vet` all reject it. The rule is positional, so one repo can have several `internal/` directories at different depths, each scoped to its own subtree. Names inside still have to be exported for the surrounding tree to use them — `internal/` decides who may import the package, not which names are visible in it.
code
text · 10 linesgithub.com/acme/svc/
cmd/api/main.go may import .../internal/store
pkg/client/client.go may import .../internal/store
internal/store/store.go
pkg/parser/parse.go may import .../pkg/parser/internal/lex
pkg/parser/internal/lex/lex.go
cmd/api may NOT import .../pkg/parser/internal/lex
github.com/other/app may NOT import .../internal/store:
use of internal package github.com/acme/svc/internal/store not allowedgo deeper
Be ready to state the rule in one sentence and to point at a small layout showing one import that compiles and one that does not. Knowing the exact error text is a bonus.
Explain that the permitted region is the tree rooted at internal/'s parent, that the go command's package loader enforces it for build, test and vet alike, and that several internal/ directories can sit at different depths in one repository.
Show that you use it deliberately: keep the supported surface small, put everything else under internal/, and be ready to say what breaks for a downstream team when a package moves in or out.
Own the consequence — an import path outside internal/ is a commitment to consumers you cannot enumerate, so the default home for new code is internal/, and promoting a package out of it is an API decision that gets reviewed as one.
## The rule, stated exactly An import of a path containing the element `internal` is disallowed if the importing code is outside the tree rooted at the parent of the `internal` directory. Every word of that matters: - **element**, not substring — a directory called `internals` or `internalstuff` is an ordinary directory with no special meaning. Only a path element that is exactly `internal` triggers the rule. - **the parent of internal/**, not `internal/` itself — the permitted region is the whole subtree that contains the `internal/` directory, not just the directory next to it. - **the importing code**, not the running code — this is a build-time decision made from import paths alone. ## Where the boundary lands Take a module whose path is `github.com/acme/svc`: ``` github.com/acme/svc/ cmd/api/main.go pkg/client/client.go internal/store/store.go ``` `internal/`'s parent is the module root, so *every* package under `github.com/acme/svc` may import `github.com/acme/svc/internal/store`: `cmd/api`, `pkg/client`, and any test in them. No package outside that path prefix may — not another repository, not another module belonging to the same team. Now push the `internal/` down a level: ``` github.com/acme/svc/pkg/parser/internal/lex ``` The parent is `pkg/parser`, so only code under `github.com/acme/svc/pkg/parser` can import `.../internal/lex`. `cmd/api` cannot, even though it lives in the same repository. A repository may contain as many `internal/` directories as it likes, each drawing its own fence; the compiler cares only about the path prefix of the importer relative to each one. ## What the failure looks like The go command reports it while loading packages, before any type checking: ``` use of internal package github.com/acme/svc/internal/store not allowed ``` There is no flag to switch it off, no build tag that exempts a file, and no way for the importing side to opt in. That is the whole point: the restriction is not negotiable by the consumer, which is what makes it a boundary rather than a suggestion. ## What internal/ does not do - **It does not make names private.** Identifiers in `internal/store` still follow the ordinary export rule — the rest of your tree can only reference names the package exports. `internal/` answers *which packages may import me*; capitalisation answers *which of my names they may then use*. - **It does not hide the source.** The code is still in the repository, still readable, still shown by documentation tooling for anyone browsing the tree. It restricts importing, not reading. - **It does not affect running or linking.** A binary built inside the tree links `internal/` packages exactly like any others. - **It is not about security.** Someone determined to reuse the code can copy it or fork the repository. It prevents accidental coupling, not deliberate copying. ## Why the toolchain bothers In Go, an import path is a public promise. Once another repository imports `github.com/acme/svc/store`, you can no longer rename that package, change its signatures, or delete it without breaking a build you do not control and cannot see — Go gives you no way to enumerate your importers. `internal/` is the mechanism that lets a repository ship a small, deliberate surface while keeping the rest changeable. It is why the standard library itself uses paths like `net/http/internal`: shared implementation code that the standard library's own packages use, and that no program outside it can reach. ## How it is used in practice The common layout is to put everything under `internal/` by default and to move a package out only when there is a real external consumer. A repository that ships several binaries places each one in `cmd/<name>/`, keeps the behaviour in packages under `internal/`, and exports at the top level only the handful of packages other teams are meant to build against. Reviewers then have an easy question to ask of any pull request that adds a top-level package: is this something we are willing to support for other people forever? If not, it belongs under `internal/`. ## Checks you can do in your head Given an import path P containing `internal`, cut P at that element: the prefix before it is the permitted root. If the importing package's own path starts with that prefix, the import compiles; otherwise it does not. Nothing else — not the module, not the repository, not the directory nesting depth — enters the decision.
- Does a function in an internal/ package still need a capital letter for the rest of the repository to call it?Yes. The two rules stack. `internal/` decides which packages are allowed to import yours; the usual export rule then decides which of your names those packages can reference. A lowercase helper in `internal/store` is still invisible to `cmd/api`.
- Where can github.com/acme/svc/pkg/parser/internal/lex be imported from?Only from the tree rooted at `github.com/acme/svc/pkg/parser` — the parent of that `internal/` directory. Packages elsewhere in the same repository, such as `cmd/api`, are outside that tree and their import is rejected, exactly as an outside repository's would be.
- Does the rule apply to test files, and to go vet?Yes. It is enforced by the go command's package loader, so `go build`, `go test` and `go vet` all apply it, and a test file is just another file in a package. A test living in a different module cannot reach an `internal/` package either.
- Can you turn the restriction off for a consumer you trust?No. There is no flag, build tag or directive that grants an exception, and the importing side cannot opt in. If a consumer genuinely needs the code, the answer is to move that package out from under `internal/` and accept it as part of your supported surface.
internal/ is a fence around one yard: everything inside the yard may use the shed, and nobody outside can reach the gate at all.
saying these in an interview costs you the question
- Says internal/ is only a naming convention teams agree to follow
- Thinks internal/ makes the identifiers inside it unexported
- Says only the directory next to internal/ may import it
- Believes a linter rather than the go command reports the violation
- Assumes a repository can have only one internal/ directory
- Claims internal/ hides the source code or provides security