Package Naming & internal/ Directories
Go package names and directory layout communicate intent before a reader opens a file.
Consistent short names plus internal/ directories keep public APIs honest and let the compiler enforce boundaries.
Summary
Package names should be lowercase, concise, and descriptive without redundant stutter.
Import paths carry the module prefix; package clauses name the compiled unit.
The special internal directory name restricts imports to ancestor directories inside the same module tree.
That combination reduces accidental coupling and makes refactors safer for library authors.
Recipe
Quick-reference recipe card - copy-paste ready.
// example.com/widget/internal/parser/parser.go
package parser // not package widgetparser
import "strings"
func Parse(raw string) (string, error) {
return strings.TrimSpace(raw), nil
}// example.com/widget/api/handler.go
package api
import "example.com/widget/internal/parser"
func Handle(raw string) (string, error) {
return parser.Parse(raw)
}When to reach for this:
- Naming a new package after its role (
auth,store,parser), not the repo name. - Placing implementation details under
internal/when external modules must not import them. - Splitting
cmd/binaries from reusable packages in the same module. - Reviewing imports in CI with
go listor linters that flag stuttering paths.
Working Example
example.com/checkout/
go.mod
cmd/checkoutd/main.go
checkout/ // domain types - import path ends in /checkout
checkout.go
internal/
tax/
tax.go
payment/
payment.go
api/
http.go
// checkout/checkout.go
package checkout
type Cart struct {
Items []string
}// internal/tax/tax.go
package tax
func Rate(region string) float64 {
if region == "CA" {
return 0.0725
}
return 0
}// api/http.go
package api
import (
"example.com/checkout/checkout"
"example.com/checkout/internal/tax"
)
func Quote(c checkout.Cart, region string) float64 {
return tax.Rate(region) * float64(len(c.Items))
}What this demonstrates:
- Public domain types live in a top-level package with a clear name.
internal/taxis callable fromapi/becauseapiis under theinternalparentexample.com/checkout.- External modules may import
checkoutandapibut notinternal/tax.
Deep Dive
How It Works
- Package names appear in qualified identifiers (
tax.Rate).
Short names read well at call sites because the import path already provides context.
- The compiler allows
internalonly when the importing package is beneath the directory that contains theinternalfolder.
For .../checkout/internal/tax, importers must live under .../checkout/.
- Multiple packages in one module each get their own subdirectory; you cannot mix
package fooandpackage barin the same folder.
Naming Rules at a Glance
| Rule | Rationale |
|---|---|
| Lowercase, no underscores | Matches Go style and import ergonomics |
No util, common, misc | Names should say what the package does |
Avoid stutter (http.HTTPServer from net/http is stdlib exception) | Call sites read cleaner |
| One package per directory | Build graph and tests stay predictable |
| Match importers' mental model | encoding/json package name json, not encodingjson |
internal/ vs pkg/
| Directory | Enforcement | Typical use |
|---|---|---|
internal/ | Compiler-enforced | Private implementation, unstable helpers |
pkg/ | Convention only | Documented public API for other repos |
| Top-level named packages | Public by default | Domain models and stable libraries |
pkg/ does not hide symbols.
If you need hard guarantees, use internal/.
Go Notes
// Bad: stuttering at call site when import is renamed poorly
import checkoutinternal "example.com/checkout/internal/checkout"
// Good: internal detail with short package name
import "example.com/checkout/internal/tax"// internal/ nested deeper still works - rule is per internal directory
// example.com/checkout/internal/payment/gateway/gateway.go
package gatewayGotchas
- Import path vs package name mismatch confuses readers. Keep them aligned except for
mainand well-known stdlib cases. internalis path-based, not visibility-based. Lowercase identifiers are already package-private;internal/blocks cross-module imports entirely.- Moving code from public to
internal/is a breaking change for anyone who imported the old path. - Generic folder names (
utils) become junk drawers. Split by responsibility instead. - Test packages
package foo_testlive besidefooand are a separate package for black-box tests.
Alternatives
- Multiple modules - Stronger release boundaries than
internal/when teams need separate semver. - Go workspaces - Local development across modules without exposing private packages.
- Interface-only public packages - Small exported surface with implementation in unexported types (still not a substitute for
internal/across modules).
FAQs
Can I name a package with an underscore?
The language allows it, but style guides and tooling expect lowercase single-word names.
Underscores are rare and distract in import blocks.
Who can import internal/tax in the example layout?
Any package whose directory is under example.com/checkout/, including api/ and cmd/checkoutd/.
Packages in other modules cannot, even if they depend on example.com/checkout.
Should the package name match the directory name?
Yes in almost all cases.
Mismatch (directory mypkg, package foo) forces mental overhead and confuses go doc.
Is pkg/ required for public libraries?
No - many projects export packages from the module root or named top-level folders.
Use pkg/ when you want a obvious "supported for external use" zone.
How do I rename a package safely?
Add the new path, re-export or migrate callers, deprecate the old import path in release notes, and remove in a major bump for libraries.
Can internal packages import each other?
Yes - internal only restricts outside importers, not siblings under the same tree.
What about internal test helpers?
Place helpers in internal/testutil or export test-only build tags sparingly.
Keep production binaries from importing test packages.
Does go vet check package names?
go vet and linters like revive can flag stuttering and style issues.
Enable them in CI for consistency.
Can I use internal across modules in one repo?
Each module has its own tree.
internal in module A does not apply to module B even if both live in one Git repo.
When is a short name like v too short?
When it obscures meaning at call sites.
Prefer version or domain-specific names unless the scope is tiny and local.
Related
- Packages and Modules: Go's Unit of Distribution - Module paths and import paths
- Standard Project Layout for Services and Libraries - cmd, internal, pkg placement
- Import Cycles, Blank Imports & Dot Imports - Import patterns and cycles
- Packages & Modules Basics - Runnable naming and internal examples
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).