skills/local/cali-coding-go-standards/SKILL.md
Use this skill for any Go (Golang) backend task — writing services, APIs, CLI tools, concurrent code, or reviewing/refactoring existing Go code. Triggers on: any .go file, mention of goroutines, channels, mutexes, Go modules, or requests to build backend systems in Go. Also triggers when setting up linters, running tests, managing Go dependencies, or running the app locally during development.
npx skillsauth add renatocaliari/agent-sync-public-skills cali-coding-go-standardsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Activate when the user works with Go backend code — writing services, APIs, CLI tools, concurrent code, or reviewing/refactoring existing Go. Also triggers on linting, testing, dependency management, or local dev tooling (Air).
Example 1: Review Go code for idiomatic correctness — check error handling, context propagation, interface design, naming, and avoid any in business logic.
Example 2: Set up a new Go project — configure Air for live reload, add golangci-lint with recommended rules, install pre-commit hooks, set up CI check workflow, and enforce file/function size limits with automated enforcement.
Example 3: Debug concurrent code — identify the correct concurrency primitive (channel, mutex, atomic, errgroup) based on the data flow pattern, and verify no race conditions.
Example 4: Refactor oversized code — a file at 480 lines needs splitting before new logic; a god function at 120 lines needs extraction into focused helpers. Propose the split structure explicitly.
_ in security-sensitive or production paths.cali-coding-go-stack.These limits apply to ALL Go projects, regardless of framework or stack. They are enforced via pre-commit hooks (see Automated Enforcement below).
No .go file shall exceed 500 lines.
handlers/chat.go → chat_send.go + chat_load.go + chat_types.godb/repository.go → session_repo.go + message_repo.go + settings_repo.goNo function shall exceed 100 lines.
sendChatMessage, HandleChat, large handler switch statements, init-like setup functions.Before writing or modifying any Go code, the agent MUST:
Check file size — if the target .go file is > 400 lines, PROPOSE a split before adding new code. Suggest the split structure explicitly (e.g., "this file has 430 lines, I suggest splitting into handler_a.go, handler_b.go"). Do NOT silently add lines to an oversized file.
Check function size — if the function being edited or any function in the hot path exceeds 80 lines, PROPOSE extraction of helper functions before modifying. Functions at 60-80 lines are yellow flags — note them.
Check for refactoring debt — when editing existing code, scan the file for: god functions (>80 lines), duplicate patterns, fmt.Sprintf with HTML tags, missing error handling. If found, propose a refactoring pass before adding new logic.
Never silently grow tech debt — if a file is 450 lines and you need to add 50 more lines, the correct action is: (a) flag the issue, (b) propose the split, (c) only after user approval, proceed with the split + new code. Adding 50 lines to a 450-line file without comment violates this standard.
These checks MUST be automated via hooks so violations are caught before commit or build, not after.
Agent MUST auto-create these hooks. When scaffolding or modifying a Go project, run bash references/setup-hooks.sh from the project root. This script handles all hook setup automatically.
setup-hooks.sh does| Layer | Scope | Type | What it checks |
|-------|-------|------|----------------|
| pi.dev hooks | Global (user machine) | Post-build (info) | File size, lint, deadcode |
| pi.dev hooks | Global (user machine) | Pre-commit (blocking) | File size, lint, tidy |
| pi.dev hooks | Global (user machine) | Pre-push (blocking) | govulncheck |
| Git hooks | Per-project | Pre-commit (blocking) | File size, lint, tidy |
| Air post_cmd | Per-project | On-save (info) | Lint (fast) |
| GitHub Actions | Per-project | CI (blocking) | Tidy, size, vet, lint, tests |
| Makefile | Per-project | Manual | File size, lint |
If pi.dev is installed (~/.pi/agent/hook/ exists), setup-hooks.sh registers global hooks that run on every Go project:
go build): file size check, golangci-lint, deadcode — non-blocking infogit commit): file size check, golangci-lint, go mod tidy — blocking (exit code 2)git push): govulncheck — blockingIf pi.dev is not detected, standard .githooks/pre-commit hooks are created instead. These work on any machine regardless of pi.dev.
setup-hooks.sh injects post_cmd into .air.toml if it exists — runs fast lint on every save (non-blocking):
[build]
post_cmd = ["golangci-lint run ./... --timeout 3m --fast || true"]
setup-hooks.sh| File | Purpose |
|------|---------|
| .githooks/pre-commit | Blocking git hook (file size, lint, tidy) |
| .air.toml (updated) | Non-blocking lint on every save |
| .github/workflows/check.yml | CI (tidy, size, vet, lint, tests) |
| Makefile (updated) | make lint target with file size check |
Handle errors at the call site. Never use _ to discard errors in production code.
Always wrap with context so stack traces are readable:
// ✅ Do
if err := store.Save(ctx, user); err != nil {
return fmt.Errorf("saving user %d: %w", user.ID, err)
}
// ❌ Don't
store.Save(ctx, user)
Sentinel errors for expected conditions; fmt.Errorf("%w") for propagation:
var ErrNotFound = errors.New("not found")
if errors.Is(err, ErrNotFound) { ... }
Define interfaces where they are used, not where the type is defined. Keep interfaces small — 1 to 3 methods. The standard library is the model.
// ✅ Do — consumer defines what it needs
type UserLoader interface {
LoadUser(ctx context.Context, id int) (*User, error)
}
// ❌ Don't — giant interface at the producer
type UserRepository interface {
Load(...)
Save(...)
Delete(...)
List(...)
Search(...)
}
Rule: Prefer io.Reader, io.Writer, fmt.Stringer over custom interfaces when stdlib suffices.
context.Context is always the first parameterAll functions that perform I/O, call external services, or may be cancelled must accept
ctx context.Context as the first argument. No exceptions.
// ✅ Do
func (s *UserService) Load(ctx context.Context, id int) (*User, error)
// ❌ Don't
func (s *UserService) Load(id int) (*User, error)
Never store a context in a struct — pass it through the call chain.
slogUse log/slog (stdlib since Go 1.21). Always include structured key-value pairs.
Never use fmt.Println or bare log.Printf for application logs.
// ✅ Do
slog.InfoContext(ctx, "user loaded", "user_id", id, "duration_ms", elapsed.Milliseconds())
slog.ErrorContext(ctx, "save failed", "user_id", id, "error", err)
// ❌ Don't
fmt.Printf("loaded user %d\n", id)
log.Printf("error: %v", err)
No init() for dependencies. No package-level vars for services.
Constructors make dependencies explicit, testable, and traceable from main.
// ✅ Do
type UserService struct {
store UserStore
logger *slog.Logger
}
func NewUserService(store UserStore, logger *slog.Logger) *UserService {
return &UserService{store: store, logger: logger}
}
// ❌ Don't
var globalUserService = &UserService{store: defaultStore}
s *Server, h *Handler, not this or selfuserID, httpClient, urlPath — not userId, HttpClient, UrlPathuser.UserID → user.ID, auth.AuthToken → auth.Tokenany / interface{}Use generics (Go 1.18+) or concrete types. any hides intent from the compiler,
static analysis, and LLMs. Use it only at true boundaries (JSON decode, plugin systems).
// ✅ Do — generic constraint
func Map[T, U any](slice []T, fn func(T) U) []U { ... }
// ❌ Don't — unconstrained any in business logic
func Process(data any) any { ... }
init() side effectsinit() is reserved for registering drivers and codecs only.
Never put business logic, config loading, or network calls in init().
They make initialization order non-obvious and untestable.
All tests for logic with multiple cases must use table-driven format:
func TestValidate(t *testing.T) {
tests := []struct {
name string
input string
want bool
wantErr bool
}{
{"empty string", "", false, true},
{"valid email", "[email protected]", true, false},
{"missing @", "invalid", false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Validate(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("wantErr=%v, got %v", tt.wantErr, err)
}
if got != tt.want {
t.Errorf("want %v, got %v", tt.want, got)
}
})
}
}
Use this priority order — do not skip levels without justification:
| Scenario | Preferred solution |
|---|---|
| Transferring data between goroutines | channel |
| Shared struct fields | sync.Mutex or sync.RWMutex |
| Simple counter or boolean flag | sync/atomic generic types |
| Fan-out with error propagation | golang.org/x/sync/errgroup |
// coordinator owns the map; workers send updates via channel
type update struct{ key string; delta int }
func coordinator(updates <-chan update) map[string]int {
counts := make(map[string]int)
for u := range updates {
counts[u.key] += u.delta
}
return counts
}
type SafeCache struct {
mu sync.RWMutex
items map[string]string
}
func (c *SafeCache) Set(key, val string) {
c.mu.Lock()
defer c.mu.Unlock() // always defer immediately after locking
c.items[key] = val
}
func (c *SafeCache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.items[key]
return v, ok
}
var requestCount atomic.Uint64 // generic, no casting needed
func handler() {
requestCount.Add(1)
}
import "golang.org/x/sync/errgroup"
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error { return fetchUser(ctx, id) })
g.Go(func() error { return fetchOrders(ctx, id) })
if err := g.Wait(); err != nil {
return fmt.Errorf("parallel fetch failed: %w", err)
}
Air is the standard live reload tool. See references/air-guide.md for:
.air.toml config, .gitignoredefer f.Close() or defer resp.Body.Close() immediately after opening.go.sum — always commit it to version control; never use GONOSUMCHECK in production.init() side effects — see Idiomatic Go Rules above.any in business logic — see Idiomatic Go Rules above.go build during a dev session — use Air; see Development Workflow above.go test -race ./...
go build -race -o app ./cmd/app
govulncheck ./...
# Install if missing:
go install golang.org/x/vuln/cmd/govulncheck@latest
go install golang.org/x/tools/cmd/deadcode@latest
deadcode -test ./...
See references/lint-config.md for:
.golangci.yml config.go file)references/gorules.go) for Datastar + Go LLM patternsgolangci-lint run ./....templ/.html files), install with go install github.com/calionauta/datastar-lint@latest and run datastar-lint -r ..golangci.yml)Beyond the defaults, enable this set for stronger static analysis. Pin
golangci-lint v2.12.2+ (older v2.x builds lack stylecheck,
modernize, tagliatelle):
linters:
enable:
- dupl # duplicated code blocks
- goconst # repeated literals -> constants
- revive # golint successor (style/structure)
- staticcheck # SCA (includes the ST* style checks - see note)
- errcheck # unchecked errors
- ineffassign # ineffective assignments
- govet # go vet (+ inline in v2)
- gocritic # opinionated suggestions
- gosec # security
- noctx # missing context.Context on request builders
- gocyclo # cyclomatic complexity
- lll # line length
- funlen # function length
- mnd # magic numbers
- tagliatelle # struct tag (json/yaml) conventions
- modernize # modern Go idioms (slices.Contains, min/max, rangeint)
- nolintlint # malformed/insufficient //nolint
v2 gotchas:
gofumpt is a formatter, not a linter - configure it under
formatters: (golangci-lint bundles it), do NOT list it under
linters.enable.stylecheck was merged into staticcheck in golangci-lint v2 (the
ST* analyzers moved there). Enabling staticcheck already covers it;
listing stylecheck errors with "unknown linter".clientID bound by a
frontend signal) should keep //nolint:tagliatelle // reason rather than
be renamed - the linter still catches future deviations.gofumptgofumpt and golangci-lint bundle different gofumpt releases. A
standalone gofumpt -l run can flag files that the CI golangci-lint run
passes (or vice-versa) because the bundled formatter versions differ. This
produces false-positive diffs that waste a CI cycle. Rule: treat
golangci-lint as the single source of truth for formatting in CI and in the
local gate. If you want a pre-commit format check, run golangci-lint run
(or make fmt that shells out to golangci-lint), not a bare gofumpt -l.
gh signoffFor projects whose CI runs a heavy matrix (multi-tag builds, -race, CGO),
run that exact gate locally before pushing so you never ship a broken
commit or wait on remote runners. gh-signoff
is a gh extension that stamps a green commit status after your local
tests pass:
gh extension install basecamp/gh-signoff
make ci-local # mirror CI: templ gen + golangci-lint + datastar-lint + css-check + tests (all tag combos) + builds
make signoff # ci-local + gh signoff -f
Advisory vs blocking: if the repo deploys on push to a branch (not
PR merge), the signoff status is a signal, not a merge gate — use
gh signoff -f (force) so it stamps before push, and do not run
gh signoff install (which would require the status for PR merge and is
meaningless for a push-to-deploy flow). If/when the workflow moves to PRs,
enable gh signoff install to make signoff a merge requirement.
go mod why <package> before adding them.golang.org/x/... (official extended stdlib) over unknown third parties.go get, run govulncheck ./... to catch newly introduced CVEs.go.mod and go.sum in sync — CI must fail if they are dirty (go mod tidy check).# CI check: fails if go.mod/go.sum are not tidy
go mod tidy
git diff --exit-code go.mod go.sum
For single-binary Go projects with PocketBase/SQLite and an LLM call, the hybrid pattern below covers 80% of regression risk with 20% of the effort. Full playbook in references/test-strategies.md.
Default strategy:
func (s *Service) streamFn func(...) and superviseFn func(...) fields; production leaves them nil, tests inject stubs. Zero external deps.pocketbase/pocketbase + SQLite, run a temp-dir PB instance in tests via pocketbase.NewWithConfig + app.Bootstrap(). Use httptest.NewRecorder() + datastar.NewSSE() to drive SSE handlers.Skip:
tools
Extrai métricas estruturadas, cálculos e estimativas de transcripts de entrevistas com clientes do Sommelier de IA. Produz um JSON com dores, frequências, tempo gasto, pessoas envolvidas, economia potencial, ROI e recomendações financeiras. Projetado para alimentar o cali-degustia-diagnostico ou integrar com dashboards/planilhas.
tools
Guia a coleta de depoimentos de clientes do Sommelier de IA no momento certo do processo, usando a abordagem de Hormozi: pedir depois da primeira evidência de resultado, nunca na entrega. Gera depoimentos mais autênticos e reduz a sensação de que o cliente está sendo "solicitado".
development
[stelow] Full UX critique for visual interfaces. Accepts a live URL, source code directory, or screenshot image. Evaluates accessibility (WCAG AA), Nielsen's 10 heuristics, visual hierarchy, cognitive load, consistency, mobile responsiveness, AI slop, emotional journey, and design personas — then generates a classified gap report. Standalone or integrated into stelow and stelow-product-testing-execution.
development
Building trust through perception and guarantee mechanisms. Covers ten pillars to materialize trust, guarantee types from unconditional to anti-guarantees, and strategic approaches for different contexts.