What does a cmd/<name>/main.go layout give a Go repo that ships five binaries?
answer
- a directory is one package
- how many func main fit in one
- the executable takes its name from somewhere
- a main package cannot be imported
- so shared code has to move out
basics
~20 sEach binary gets its own directory holding a package main, because a directory is one package and can declare func main only once. The directory name becomes the executable name, and shared code moves to an importable package.
solid answer
~50 sIn Go a directory is a package, so five binaries need five directories: `cmd/api`, `cmd/worker`, `cmd/migrate` and so on, each with its own `package main` and one `func main`. Two entry points cannot share a directory, because they would be one package with two `main` functions. The layout also names things for you — `go build ./cmd/worker` writes an executable called `worker`, after the directory — and `go build ./...` covers all five in one command. The second thing it buys is a forcing function: a `main` package can never be imported, so anything two binaries share has to move out of `cmd/` into a real package, normally `internal/<responsibility>`. That keeps `main` thin — flag and environment parsing, wiring dependencies together, start and shutdown — and keeps the behaviour in packages that tests and the other binaries can import.
code
text · 12 linesrepo/
cmd/api/main.go package main
cmd/worker/main.go package main
cmd/migrate/main.go package main
cmd/backfill/main.go package main
cmd/lint/main.go package main
internal/store/
internal/config/
go build ./cmd/worker -> executable named worker
go build ./... -> builds all five
cmd/worker cannot import cmd/api: a main package is not importablego deeper
Be ready to say that a directory is a package, that each binary therefore needs its own directory with one func main, and that the binary is named after the directory.
Explain why a main package cannot be imported and what that forces: shared code has to move into an importable package, normally under internal/, and main shrinks to config, wiring and shutdown.
Show the review instinct — spot logic that has accumulated in main, name where it should go, and know that a non-main package under cmd/ is importable by anyone unless it is under internal/.
Own the split itself: how many binaries the repository should ship, what they share, and whether a second program deserves its own cmd/ entry point or belongs behind a subcommand of an existing one.
## Why the directory, not the file, is the unit Go has no notion of a file-level entry point. All `.go` files in one directory belong to one package, and the package named `main` is the one the linker turns into an executable, using its `func main` as the entry point. That single rule produces the whole convention: a repository that ships five programs needs five directories each containing `package main`, because two `func main` declarations in one directory would be a redeclaration in the same package. `cmd/` is simply the agreed place to keep those directories together. ``` repo/ cmd/api/main.go package main cmd/worker/main.go package main cmd/migrate/main.go package main internal/store/ internal/config/ ``` ## What the toolchain does with it - `go build ./cmd/worker` writes an executable named after the package's directory — `worker` — rather than after the file or the package name. That is why the directory names are the product names. - `go build ./...` and `go test ./...` walk the whole tree, so all five programs are covered by one command in CI. - `go install ./cmd/...` builds every main package under `cmd/` at once, which is what a release job usually runs. None of this is required by the go command: a repository with a single binary is perfectly idiomatic with `package main` at its root. `cmd/` earns its place when there is more than one program, or when the root of the repository is a library other people import and you do not want a binary sitting in the middle of it. ## The forcing function A `main` package cannot be imported by anything — the language forbids it. So the moment two binaries need the same code, the layout gives you no lazy option: the shared code must move into a package neither of them owns. In a repository with an `internal/` tree the natural home is `internal/<responsibility>` — `internal/store`, `internal/retry`, `internal/config` — where it is importable by all five binaries and by nobody outside the repository. This is the quiet benefit of the layout. Teams that put logic directly into `main` discover, the second time they need it, that there is no way to reuse it except by copying. Teams that keep `main` to wiring find the extraction has already happened. ## What belongs in main A good `func main` reads like a wiring diagram: - parse flags and environment into a config value; - construct the dependencies (a store, a client, a logger); - start the thing (a server, a consumer loop, a one-shot job); - wait for a signal, shut down, and turn any error into an exit status. Everything else — the request handling, the query, the retry policy, the parsing — belongs in ordinary packages, for two reasons. First, other binaries can then use it. Second, it is directly testable from an ordinary test in that package, rather than only through the assembled program. ## Non-main packages under cmd/ A directory under `cmd/` that is *not* `package main` is an ordinary importable package: `cmd/api/handlers` can be imported by anything, including another repository, unless it sits under an `internal/` directory. That surprises people who assume `cmd/` is itself a privacy boundary. It is not; only `internal/` and the `main` package rule provide any enforcement. If you want a helper that only `cmd/api` uses to stay unreachable, either keep it unexported inside the `main` package itself or put it under `internal/`. ## Common layout mistakes - **One giant main.** Several hundred lines of behaviour in `func main`, none of it importable, none of it directly testable. - **Sharing through cmd/.** Trying to import `cmd/api` from `cmd/worker`; the compiler refuses because it is a `main` package, and the usual reaction — copying the file — is worse than extracting a package. - **Naming the directory after the package instead of the product.** The directory name is what users type, so `cmd/svc-api` yields a binary called `svc-api`; pick the name you want to ship. - **A `cmd/` directory holding one binary in a repository that ships one binary.** Harmless, but it buys nothing over `package main` at the root. ## What an interviewer is checking Mostly that you know the directory-is-a-package rule and can derive the convention from it rather than reciting a layout you copied. The follow-up is usually about where shared code goes, which is where the real design conversation starts.
- Does a repository that ships one binary need a cmd/ directory?No. `package main` at the repository root is idiomatic and simpler. `cmd/` starts paying for itself with the second binary, or when the root is a library other teams import and you would rather not have a program sitting among its packages.
- Can another package import cmd/api?Not if it is `package main` — the language forbids importing a main package. A non-main package under `cmd/`, such as `cmd/api/handlers`, is importable by anything unless it also sits under an `internal/` directory; `cmd/` is not itself a privacy boundary.
- What do you keep in func main and what do you push out?Keep flag and environment parsing, dependency construction, startup, signal handling and the exit status. Push the behaviour out into packages, both so the other binaries can import it and so tests can exercise it directly instead of through the assembled program.
saying these in an interview costs you the question
- Thinks the go command requires a cmd/ directory
- Believes two func main declarations can share one directory
- Tries to import one cmd/ binary's main package from another
- Assumes the executable is named main.go or main
- Leaves the behaviour in main so nothing else can reuse or test it
- Treats cmd/ as a privacy boundary like internal/