scaffold-go-service
Use when starting a Go HTTP service or adding one to a repo with no Go module yet — scaffolds stdlib net/http (1.22+ routing), cmd/ and internal/ layers, slog JSON logs, validated env config, problem+
SKILL.mdskills/backend/scaffold-go-service/SKILL.md
GitHubScaffold Go Service
Rationale and rules: go-patterns.md. Full file contents: go-files.md. Shared standards: service-layout.md, config.md, stack-versions.md.
1. Audit (change nothing)
cat .claude/stack-profile.md ~/.claude/stack-profile.md 2>/dev/null # backend.track, database.orm, tests.layout, task_runner, observability
ls go.mod go.work .golangci.yml .golangci.yaml cmd internal 2>/dev/null
head -3 go.mod 2>/dev/null; go version; golangci-lint --version 2>/dev/null
grep -rlE 'go-chi/chi|gin-gonic/gin|labstack/echo|gofiber' --include=go.mod . 2>/dev/null
Read the profile first; detect only what it leaves open (stack-profile.md).
backend.trackis set and is notgo: stop and name the track the profile says; suggest the matching scaffold skill.backend.trackis unset and a router framework (chi,gin,echo,fiber) is already ingo.mod: keep it, adapt step 5 (routes registered on that router). Do not ask.- No Go toolchain: tell the user which Go line to install (stack-versions.md); do not scaffold blind.
2. Decide
- No
go.mod: full scaffold (steps 3 to 7). The module path comes fromgit remote get-url origin(github.com/<org>/<repo>), else ask once; never leaveexample.com/svc. go.modpresent, nocmd/<svc>orinternal/: add the layers around the existing code; do not move existing packages without being asked.- All present and step 7 passes: report "already in place" and stop.
3. Detect track
| Question | Source | Default |
|---|---|---|
| Router | profile or go.mod |
stdlib http.ServeMux with GET /notes/{id} patterns. chi only if already used, see go-patterns.md |
| Service name | repo folder name, kebab-case | svc in the files becomes this name |
| Database | database.orm |
sqlc: add internal/repository/db/ (sqlc output) and a pgxpool seam in internal/platform/db/ via the sqlc skill of this catalogue; then a readiness check. none: skip |
| Tests | tests.layout |
unit tests always *_test.go beside the code (Go convention); tests-dir applies to integration tests in tests/ |
| Observability | observability.otel |
true: bootstrap the SDK in internal/platform/ before the logger (observability.md) |
| Task runner | task_runner |
just: recipes run, test, lint, build that call the commands in step 7 |
4. Install only what is missing
go mod init <module-path> # only when go.mod is missing; writes the current go line
brew install golangci-lint # or mise / the release binary; confirm it prints v2.x
The scaffold has no third-party runtime dependencies. Check golangci-lint --version against stack-versions.md; v1 configs do not load in v2.
5. Generate the seams
Create the files from go-files.md, skipping any that exist. Replace example.com/svc with the module path in every import.
cmd/<svc>/main.go composition root, signal handling, graceful shutdown
internal/config/config.go Load(lookup) -> Config, every invalid key reported
internal/platform/logging/logging.go slog JSON, contract field names, redaction
internal/transport/httpapi/{server,problem}.go routes, middleware, problem+json, readiness
internal/service/*.go use cases, no net/http
internal/repository/*.go storage; the only layer with SQL
.golangci.yml .env.example
*_test.go beside each package
Rename notes to the first real module, or keep it as the worked example until one exists.
6. Wire
.gitignore: add/bin/,.env,coverage.out.- Local run:
set -a; . ./.env; set +a; go run ./cmd/<svc>. Go does not read.env; see config.md. - Readiness: add one
httpapi.ReadinessCheck{Name, Check}per dependency inmain.go.Checkreceives a context with a 2 s deadline. - Shutdown: put pool close and telemetry flush after
srv.Shutdown, inrun(). KeepReadHeaderTimeoutset: the zero value ofhttp.Serverhas no timeouts. - Request context: handlers and services pass
r.Context()down; repositories takectxfirst.BaseContextties every request to the signal context. go.mod:go mod tidyonce; commitgo.sum; CI runsgo mod verify(version-protocol.md).- OpenAPI is not generated here: stdlib handlers carry no schema. Add
oapi-codegenor a hand-keptopenapi.yamlwhen clients need one. - CI, Docker and deploy are out of scope; the
devcoreandinfraskillsplugins own them.
7. Verify
gofmt -l . # expect: no output
go vet ./... && go test ./... # expect: ok for config, logging, httpapi
golangci-lint config verify && golangci-lint run ./... # expect: 0 issues
go build -o bin/<svc> ./cmd/<svc> && PORT=8131 ./bin/<svc> & # expect JSON line "message":"listening"
curl -s localhost:8131/readyz # expect {"status":"ok"}
curl -si localhost:8131/nope # expect 404 and Content-Type: application/problem+json
PORT=abc ./bin/<svc> # expect "fatal: config: PORT: ..." and exit 1
kill -TERM %1 # expect "shutting down" in the log, exit code 0
A second run of this skill finds everything in place and changes nothing.
References
- go-patterns.md: why each choice, when to deviate.
- go-files.md: the files.
- service-layout.md, config.md.
- logging-contract.md, security-baseline.md.