# Bootstrap — Project Layout, Toolchain, Taskfile, CI What every new Go project gets in the first 60 seconds. Drop the script in `scripts/go/new-project.go` does all of this — this document explains *what* it produces and *why*. ## Toolchain pin `go.work` (monorepo) or just rely on `go.mod`'s `go 1.23` directive (single module). Go 1.21+ auto-downloads matching toolchain when the local `go` binary is older. **No `.tool-versions` / `asdf` / `mise` indirection required** unless your shop standardizes on it. ```bash # Confirm a working toolchain go env GOTOOLCHAIN # should be "auto" or your pinned version go version # ≥ 1.23 ``` ## Required global installs These are CLI tools, installed once per machine via `go install`: ```bash go install mvdan.cc/gofumpt@latest go install golang.org/x/tools/cmd/goimports@latest go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.0.0 go install go.uber.org/nilaway/cmd/nilaway@latest go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest go install github.com/pressly/goose/v3/cmd/goose@latest go install go.uber.org/mock/mockgen@latest go install github.com/go-task/task/v3/cmd/task@latest ``` For Connect/protobuf projects, additionally: ```bash go install github.com/bufbuild/buf/cmd/buf@latest go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest ``` ## Project layout — the canonical tree ``` myservice/ ├── go.mod ├── go.sum ├── Taskfile.yml # task runner ├── .golangci.yml # see golangci-strict.md ├── .editorconfig ├── .gitignore ├── README.md ├── AGENTS.md # agent-readable project facts ├── cmd/ │ └── server/ │ └── main.go # ONLY: parse flags, call cmd.Execute(); ≤ 50 LOC ├── internal/ # NEVER importable from outside this module │ ├── api/ # transport layer (gin/connect routers) │ │ ├── server.go # gin engine setup, route registration │ │ ├── middleware/ │ │ │ ├── request_id.go │ │ │ ├── logging.go │ │ │ └── auth.go │ │ └── handlers/ │ │ ├── users.go │ │ └── users_test.go │ ├── domain/ # parse-don't-validate types, smart constructors │ │ ├── user.go │ │ └── email.go │ ├── service/ # business logic, depends on domain only │ │ └── user_service.go │ ├── store/ # persistence; sqlc-generated code lives here │ │ ├── sqlc/ # sqlc-generated, do not hand-edit │ │ ├── queries/ # *.sql files sqlc reads │ │ └── migrations/ # goose migrations │ ├── config/ # env-driven config (caarlos0/env) │ │ └── config.go │ └── obs/ # observability: slog setup, otel, healthz │ └── logger.go ├── pkg/ # exportable libraries — only if you publish │ └── … ├── proto/ # *.proto definitions (Connect/gRPC projects) │ └── service.proto ├── gen/ # generated code (Connect, OpenAPI) │ └── service/v1/ │ ├── service.pb.go │ └── servicev1connect/ ├── test/ # cross-cutting test helpers, fixtures └── .github/workflows/ci.yml ``` **Rules**: - `cmd//main.go` is ≤ 50 LOC. Anything more lives in `internal/cmd/`. - `internal/` is **the** business code. Other modules cannot import it (Go compiler-enforced). - `pkg/` is for things you genuinely want third parties to import. Empty until proven otherwise. - No `utils/`, `helpers/`, `common/`, `shared/`. **REJECT.** Files are named after the concept they own. - One package per directory. One responsibility per package. ## `Taskfile.yml` — the entry point for every action `go-task/task` is the modern Make replacement. Cross-platform, YAML, fast. ```yaml version: '3' vars: BINARY: server PKG: ./cmd/server tasks: default: deps: [fmt, lint, test] fmt: desc: Format all Go files cmds: - gofumpt -w . - goimports -w -local "$(go list -m)" . lint: desc: Run all linters cmds: - golangci-lint run --timeout 5m ./... - nilaway -include-pkgs "$(go list -m)/..." ./... test: desc: Run tests with race detector cmds: - go test -race -shuffle=on -count=1 ./... test-cover: desc: Coverage report cmds: - go test -race -shuffle=on -count=1 -coverprofile=coverage.out ./... - go tool cover -html=coverage.out -o coverage.html build: desc: Build the binary cmds: - go build -trimpath -ldflags="-s -w" -o bin/{{.BINARY}} {{.PKG}} run: desc: Run the server locally deps: [build] cmds: - ./bin/{{.BINARY}} gen: desc: Run all code generators cmds: - task: gen:sqlc - task: gen:mocks - task: gen:proto gen:sqlc: cmds: - sqlc generate sources: - internal/store/queries/*.sql - internal/store/sqlc.yaml generates: - internal/store/sqlc/*.go gen:mocks: cmds: - go generate ./... gen:proto: cmds: - buf generate sources: - proto/**/*.proto - buf.yaml - buf.gen.yaml migrate:up: cmds: - goose -dir internal/store/migrations postgres "$DATABASE_URL" up migrate:down: cmds: - goose -dir internal/store/migrations postgres "$DATABASE_URL" down ci: desc: Everything CI does, locally deps: [fmt, lint, test, build] ``` `task` (no args) runs format + lint + test in parallel where possible. `task ci` runs the full pipeline. ## `go.mod` template ```go module github.com/your-org/myservice go 1.23 require ( github.com/caarlos0/env/v11 v11.2.2 github.com/gin-gonic/gin v1.10.1 github.com/go-playground/validator/v10 v10.22.1 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.6 golang.org/x/sync v0.18.0 ) ``` Only direct deps listed; `go mod tidy` populates indirects. ## `.editorconfig` ```ini root = true [*] indent_style = tab indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.{yml,yaml,json,md}] indent_style = space indent_size = 2 ``` ## `.gitignore` ```gitignore bin/ coverage.out coverage.html *.test *.prof # IDE .idea/ .vscode/ *.swp # Local env .env .env.local # Secrets *.pem *.key ``` ## CI — minimal GitHub Actions `.github/workflows/ci.yml`: ```yaml name: ci on: pull_request: push: branches: [main] jobs: ci: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.23' cache: true - name: Install tools run: | go install mvdan.cc/gofumpt@latest go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.0.0 go install go.uber.org/nilaway/cmd/nilaway@latest go install github.com/go-task/task/v3/cmd/task@latest - name: Format check run: gofumpt -l . | (! grep .) - name: Lint run: golangci-lint run --timeout 5m ./... - name: Nilaway run: nilaway ./... - name: Test run: go test -race -shuffle=on -count=1 ./... - name: Build run: go build -trimpath ./... ``` The order matters: format → lint → nilaway → test → build. Fail fast on the cheap checks. ## `AGENTS.md` — agent-readable project facts Every new project gets an `AGENTS.md` at the root. The content is **machine-friendly**: short, declarative, no marketing prose. Example: ```markdown # AGENTS.md Go 1.23+ HTTP service for {one-line purpose}. ## Commands - `task` — fmt + lint + test - `task build` — produce ./bin/server - `task gen` — regenerate sqlc + mocks + proto ## Architecture - `cmd/server/main.go` — entrypoint, ≤50 LOC - `internal/api/` — gin handlers + middleware - `internal/domain/` — smart-constructor types, no I/O - `internal/store/sqlc/` — generated; never hand-edit ## Conventions - `slog` for all logs; never `log.*`, never `fmt.Println` - `context.Context` first arg for every public function - Errors wrapped with `%w`; check with `errors.Is/As` - 250 pure LOC ceiling per file — split before adding lines ``` The skill's `cmd/new-project.go` writes this file with project-specific values filled in. ## Sources - Go modules reference: https://go.dev/ref/mod - go-task: https://taskfile.dev - golangci-lint v2: https://golangci-lint.run/docs/configuration/ - Standard project layout debate: https://go.dev/doc/modules/layout (NOT `golang-standards/project-layout` — that repo is community, not official)