Stage 7 · Master
Phase 1 — Project Foundation
golangci-lint
Configure one shared lint contract every module runs against, and use it to start enforcing the internal-package boundary rule review alone was carrying until now.
One Lint Config, Four go.mod Files
golangci-lint reads its configuration relative to wherever it is invoked, not from a repository-wide root the way some tools assume. Because this repository has no root go.mod, .golangci.yml is placed at the repository root anyway — as project-wide policy, not as something tied to a module — and every module invocation points at it explicitly by absolute path, rather than each module keeping its own copy that could silently drift out of sync.
Choosing Linters Deliberately, Not Enabling Every Linter Available
version: "2"
linters:
default: none
enable:
- errcheck
- govet
- staticcheck
- unused
- gocritic
- revive
- depguard
settings:
depguard:
rules:
handler-must-not-import-repository:
files:
- "**/internal/handler/**"
deny:
- pkg: "**/internal/repository"
desc: "handlers must call internal/service, never internal/repository directly (see the Folder Structure lesson)"
run:
timeout: 3m
errcheck, govet, staticcheck, and unused catch the categories of bug most Go codebases actually ship — an ignored error, a suspicious construct, dead code. gocritic and revive add readability-oriented checks without the noise of enabling every available linter, several of which actively disagree with idioms this course otherwise recommends (for example, some style linters discourage the very functional-options-free constructor pattern the Dependency Injection lesson relies on).
depguard: Turning a Review Convention Into an Automated Check
The Folder Structure lesson admitted one boundary the Go compiler does not enforce: a handler calling a repository directly, bypassing the service layer. depguard's rules let any file matching a glob (here, everything under internal/handler) deny importing a specific package path — organization/internal/repository — with a custom message explaining why, surfaced directly in the linter's output instead of only in a code reviewer's memory.
Extending the Makefile From the Previous Lesson
-.PHONY: help tidy build test vet run-organization compose-up compose-down compose-logs
+.PHONY: help tidy build test vet lint run-organization compose-up compose-down compose-logs
help:
@echo "Modules discovered: $(MODULES)"
- @echo "Targets: tidy build test vet run-organization compose-up compose-down compose-logs"
+ @echo "Targets: tidy build test vet lint run-organization compose-up compose-down compose-logs"
+
+lint:
@for m in $(MODULES); do \
echo "==> lint $$m"; \
(cd $$m && golangci-lint run --config $(CURDIR)/.golangci.yml ./...) || exit 1; \
done
Only the three changed regions are shown. Reprinting tidy, build, test, vet, and Compose recipes would add no new knowledge. lint is the one target that uses cd because golangci-lint, unlike the go tool, has no -C flag.
Running Lint Clean, Then Deliberately Breaking It
func demo() {
x := 42 // declared and unused — deliberately, to see the linter catch it
}
pkg/apperr/apperr.go:12:2: declared and not used: x (unused)
Applied exercise
Trigger the depguard rule instead of a generic unused-variable finding
The demonstration above shows a generic linter finding. Trigger the specific architectural rule this lesson added.
- internal/handler does not exist with real code yet (it arrives in Phase 2), so instead: temporarily add a file at pkg/apperr/scratch_handler_demo.go under a fake path structure — or more simply, read depguard's rule closely and explain in your own words, referencing the exact glob and deny rule in .golangci.yml, what file path and import combination would trigger it once Phase 2's internal/handler package exists.
- Write out the exact golangci-lint finding you would expect to see, based on the desc string in the config.
- Remove any temporary files created for this exercise.
Deliverable
A short written explanation of exactly which future file and import combination the depguard rule targets, plus the expected finding text.
Completion checks
- The explanation correctly identifies internal/handler/** as the constrained file glob.
- The explanation correctly identifies internal/repository as the denied import.
- No temporary files remain in the repository afterward.
What does the depguard rule added in this lesson actually enforce, and how is that different from the internal/ visibility rule from the Folder Structure lesson?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.