From Source to Service
Project layout, modules & tooling
Lesson 2 of 5
What you'll learn
- Understand
go modas the (much smaller) package.json equivalent - Lay out a service with
cmd/for binaries andinternal/for private code - Run the standard tooling gauntlet:
gofmt,go vet,staticcheck
A Go project starts with one command:
// go mod init github.com/you/notesvc
// creates go.mod:
module github.com/you/notesvc
go 1.22
go.mod is package.json without scripts, without devDependencies, without a lockfile debate (go.sum is generated and committed). The module path doubles as the import prefix: code in internal/store is imported as github.com/you/notesvc/internal/store. Adding a dependency is just importing it and running go mod tidy — which also removes anything unused.
The layout that Go expects
There's no framework scaffold, but production Go repos converge on the same shape:
notesvc/
go.mod
cmd/
notesvc/
main.go // wiring only: flags, config, mux, ListenAndServe
internal/
store/ // data access
api/ // handlers (the closures from lesson 1)
Two conventions do the heavy lifting. cmd/<name>/main.go holds one main package per binary you ship — main stays thin, just wiring. Everything else lives in internal/, and this one is not a convention: the compiler rejects any import of an internal/ package from outside the module subtree that contains it. It's private enforced at build time — no #[internal] JSDoc pleading, no eslint rule.
No src/, no barrel files
Go has no src/ directory and no index-file re-export pattern. A directory is a package; its exported identifiers (capitalized names, from the primer) are its entire public API. Deep import paths are normal and idiomatic.
The tooling gauntlet
Three tools run in every serious Go CI pipeline, and locally before you commit:
// gofmt -l . one true style; there is nothing to configure
// go vet ./... catches real bugs: bad printf verbs, copied locks,
// unreachable code — ships with the toolchain
// staticcheck ./... third-party (go install honnef.co/go/tools/cmd/staticcheck@latest)
// the community linter: dead code, misuse of APIs, simplifications
gofmt ends the formatting conversation the way Prettier tried to — except there are zero options, so there is nothing to argue about. go vet is closest to typescript-eslint's correctness rules. staticcheck is the one install-it-yourself tool worth mandating.
The challenge models the part of this that surprises people: internal/ visibility. The rule is that a/b/internal/c may only be imported by code rooted at a/b.
Run it. This models the compiler's internal/ rule: an import is legal only if the importer lives inside the directory that contains the internal/ folder. Watch which imports the 'compiler' rejects.
What enforces that code outside your module can't import internal/store?
Next: the service needs to remember things — SQLite through database/sql, and the cgo-or-pure-Go driver decision.
Saved on this device. Sign in to sync your progress everywhere.