packages/shared-skills/skills/programming/references/go/bootstrap.md
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.
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.
# Confirm a working toolchain
go env GOTOOLCHAIN # should be "auto" or your pinned version
go version # ≥ 1.23
These are CLI tools, installed once per machine via go install:
go install mvdan.cc/gofumpt@latest
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/golangci/golangci-lint/cmd/[email protected]
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:
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
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/<binary>/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.utils/, helpers/, common/, shared/. REJECT. Files are named after the concept they own.Taskfile.yml — the entry point for every actiongo-task/task is the modern Make replacement. Cross-platform, YAML, fast.
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 templatemodule 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.
.editorconfigroot = 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
.gitignorebin/
coverage.out
coverage.html
*.test
*.prof
# IDE
.idea/
.vscode/
*.swp
# Local env
.env
.env.local
# Secrets
*.pem
*.key
.github/workflows/ci.yml:
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/[email protected]
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 factsEvery new project gets an AGENTS.md at the root. The content is machine-friendly: short, declarative, no marketing prose. Example:
# 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.
golang-standards/project-layout — that repo is community, not official)