Package Naming & internal/ Directories
Go package names and directory layout communicate intent before a reader opens a file.
Search across all documentation pages
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.
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.
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:
auth, store, parser), not the repo name.internal/ when external modules must not import them.cmd/ binaries from reusable packages in the same module.go list or linters that flag stuttering paths.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:
internal/tax is callable from api/ because api is under the internal parent example.com/checkout.checkout and api but not internal/tax.tax.Rate).Short names read well at call sites because the import path already provides context.
internal only when the importing package is beneath the directory that contains the internal folder.For .../checkout/internal/tax, importers must live under .../checkout/.
package foo and package bar in the same folder.| 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 |
| 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/.
// 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 gatewaymain and well-known stdlib cases.internal is path-based, not visibility-based. Lowercase identifiers are already package-private; internal/ blocks cross-module imports entirely.internal/ is a breaking change for anyone who imported the old path.utils) become junk drawers. Split by responsibility instead.package foo_test live beside foo and are a separate package for black-box tests.internal/ when teams need separate semver.internal/ across modules).The language allows it, but style guides and tooling expect lowercase single-word names.
Underscores are rare and distract in import blocks.
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.
Yes in almost all cases.
Mismatch (directory mypkg, package foo) forces mental overhead and confuses go doc.
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.
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.
Yes - internal only restricts outside importers, not siblings under the same tree.
Place helpers in internal/testutil or export test-only build tags sparingly.
Keep production binaries from importing test packages.
go vet and linters like revive can flag stuttering and style issues.
Enable them in CI for consistency.
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 it obscures meaning at call sites.
Prefer version or domain-specific names unless the scope is tiny and local.
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).
Reviewed by Chris St. John·Last updated Jul 18, 2026