Before this:Packages & modules
Structuring a Go project
Key takeaways
Real Go repositories follow a few conventions. Each executable gets a tiny main
package under cmd/. Private implementation packages live under internal/,
which the compiler forbids outside code from importing. Each package owns one
responsibility and is named for it. GopherTrunk’s own tree is a textbook example —
cmd/gophertrunk plus internal/dsp, internal/scanner, internal/sdr, and more.
Where does code go? Go has strong conventions, and following them makes any project navigable. This lesson maps them onto GopherTrunk’s real layout, extending Packages & modules.
cmd/ — one directory per binary
Every executable is a package main with a main() function, and each lives in its
own directory under cmd/. GopherTrunk ships several:
cmd/
gophertrunk/ # the main scanner binary
voice-calibrate/ # a calibration helper
encodetest/ # a codec test harness
Each main stays thin — parse flags, wire things up, call into the real packages.
go build ./cmd/gophertrunk builds just that one.
internal/ — enforced-private packages
The bulk of the code lives under internal/, and the toolchain gives that name a
superpower: nothing outside the module can import it. That lets the project expose
a deliberate surface while keeping implementation free to change:
internal/
dsp/ # signal processing — filters, down-converters
scanner/ # control-channel decode, trunking follow
sdr/ # radio hardware drivers
config/ # loading and validating configuration
api/ # the HTTP/JSON surface
One package, one responsibility
Notice the pattern in those names: each package is a single concern, named for what it
does — dsp does signal processing, sdr talks to hardware, config handles
configuration. This keeps dependencies flowing one way (a command imports scanner,
scanner imports dsp and sdr) and makes the whole tree readable.
| Directory | Holds | Importable by |
|---|---|---|
cmd/<name>/ |
One main per binary |
(it’s the top; imports others) |
internal/<pkg>/ |
Private implementation | Only this module |
pkg/ (if present) |
Deliberately public API | Anyone |
| module root | go.mod, go.sum, top-level docs |
— |
Applying it
When you start a project, resist one giant package. Put each binary under cmd/, put
shared logic in small internal/ packages named by responsibility, and let the
internal boundary keep your public surface honest. Reading a codebase laid out this
way — which is exactly what the
Reading a real Go codebase lesson does with
GopherTrunk — becomes a matter of following the imports.
Quick check: what's special about a package under internal/?
Recap
- Each binary is a thin
package mainundercmd/<name>/. internal/packages are private — only the same module can import them.- Each package owns one responsibility and is named for it.
- GopherTrunk’s
cmd/+internal/dsp,scanner,sdr,configtree is the model.
Next up: the conventions that make Go code idiomatic — from naming to error handling.
Frequently asked questions
What is the internal directory for in a Go project?
internal is a special directory the Go toolchain enforces: packages under internal/ can only be imported by code inside the same module. It’s how a project keeps its implementation packages private, exposing a deliberate surface while forbidding outside code from depending on internals it might break.
Why put each command under cmd/?
A repository often ships more than one binary — the main app, a calibration tool, a test harness. Giving each its own package under cmd/ keeps every main() small and separate, while the real logic lives in importable packages under internal/ that all the commands can share.