Project layout and workspaces¶
Go is opinionated about a few directory conventions and deliberately silent about the rest. This article covers the layout patterns the tooling actually understands, and workspaces for developing several modules at once.
The conventions the tooling enforces¶
Two directory names have real meaning to the go command:
internal/— packages under aninternal/directory can be imported only by code rooted atinternal/'s parent. It's compiler-enforced privacy at the package-tree level.testdata/— ignored by the build tooling; a place for test fixtures.
example.com/shop/
├── go.mod
├── internal/
│ └── auth/ # importable only within example.com/shop/...
└── store/
└── testdata/ # fixtures, ignored by the compiler
Everything else about layout is convention, not rule.
The internal/ rule is compiler-enforced — an import from outside the
parent subtree fails:
// from a different module, importing example.com/shop/internal/auth
import _ "example.com/shop/internal/auth"
// compile error: use of internal package example.com/shop/internal/auth not allowed
cmd/ and the common layout¶
A widely used (but optional) shape separates entry points from library code:
cmd/<name>/— one directory per executable, each with its ownpackage main. The directory name becomes the binary name.internal/— private packages, the bulk of the code.- top-level packages — the module's public API, if it's meant to be imported.
myapp/
├── go.mod
├── cmd/
│ ├── server/main.go # builds the "server" binary
│ └── cli/main.go # builds the "cli" binary
├── internal/
│ ├── store/
│ └── auth/
└── api/ # exported, importable by others
Build a specific command with its path:
From Python: there's no
src/requirement and no__init__.py. A directory is a package by virtue of its.gofiles;cmd/andinternal/are the rough analogues of a scripts/entrypoints folder and a private subpackage.
Keep main thin¶
A strong convention: package main should do as little as possible — parse
flags, wire things together, call into internal/ packages — so the real
logic stays testable and importable. The binary is glue; the packages are
the program.
Workspaces: developing multiple modules together¶
When you're changing two modules at once — say an app and a library it
depends on — editing go.mod with a replace for each works but is
fiddly and easy to commit by accident. A workspace solves this with a
go.work file that tells the go command to use several local modules
together.
Now, building or testing from anywhere in the workspace resolves imports of
./lib to your local checkout — no replace directives needed. Add more
with go work use ./other.
The key practice: go.work is local-only. It's for your machine's
multi-module dev loop, so it's typically git-ignored, never published.
Released builds still resolve dependencies through go.mod/go.sum.
Quick reference¶
| Path / file | Meaning |
|---|---|
internal/ |
importable only within the parent module subtree |
testdata/ |
test fixtures, ignored by the build |
cmd/<name>/ |
one executable per subdirectory (package main) |
go build ./cmd/x |
build a specific command |
go.work (go work init/use) |
use several local modules together |
keep main thin |
logic lives in importable packages |