skills/local/cali-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-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.
_ in security-sensitive or production paths.cali-go-stack.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 for local development. It watches .go and
.templ files and automatically runs pre-build hooks, recompiles, and restarts the
server on every save — no manual rebuild steps needed.
go install github.com/air-verse/air@latest
.air.toml — standard config (projects with Templ)Place this file at the project root:
[build]
pre_cmd = ["templ generate"]
cmd = "go build -ldflags='-w -X main.Version=dev' -o ./tmp/app ./cmd/web"
bin = "./tmp/app"
include_ext = ["go", "templ"]
exclude_dir = ["tmp", "vendor", "node_modules", "testdata"]
exclude_regex = ["_templ\\.go$"] # prevents rebuild loop from generated files
kill_delay = "500ms"
send_interrupt = true
[log]
time = true
[misc]
clean_on_exit = true
For projects without Templ, remove the pre_cmd line entirely.
air # run from the project root; keep the terminal open while developing
Air runs for the duration of a dev session. Stop it with Ctrl+C when done.
go build + ./app manually during a dev session..air.toml with Templ: include_ext must contain "templ",
exclude_regex must exclude "_templ\\.go$", and pre_cmd must be
["templ generate"].pgrep -x air
air before testing the change.tmp/ to .gitignore — Air writes the compiled binary there.echo "tmp/" >> .gitignore
Air builds silently — a failed build does not show an error unless you check. After starting or restarting Air, always verify:
# 1. Confirm the binary was built (no staleness)
ls -la ./tmp/app 2>/dev/null && echo "BUILT OK" || echo "BUILD FAILED"
# 2. Check the build error log for details if it failed
cat ./tmp/build-errors.log 2>/dev/null || echo "no errors log"
# 3. Confirm the server is listening on its port
lsof -i :<PORT> 2>/dev/null || echo "NOT LISTENING"
The most common Air failure: go build -o ./tmp/main . (Air's default when no
.air.toml exists) compiles the module root — which produces nothing when the
real main.go lives in cmd/web/main.go. The build exits code 0, no binary is
created, and Air silently never starts the server. Port stays empty, no error shown.
Fix: the project MUST have an .air.toml at root with:
[build]
cmd = "go build -o ./tmp/app ./cmd/web" # <-- point at the real entrypoint
bin = "./tmp/app"
include_ext = ["go", "tpl", "tmpl", "html", "templ"]
exclude_regex = ["_templ\\.go$"]
.air.toml exists and build.cmd points to the correct entrypointairlsof -i :<PORT> that the server is listeningtmp/build-errors.log for the actual compile errorNever use Air outside of local development. For deploying or restarting the app in staging or production, follow this priority order:
ko (recommended for all Go projects)ko is the 2026 standard for Go OCI image builds:
ko compiles the Go binary and wraps it in a minimal distroless imageGOOS/GOARCH without QEMU (5-10x faster than Docker multi-arch)//go:embed (embed.FS) — the binary must be self-contained# Build and push to registry (e.g. GitHub Container Registry)
KO_DOCKER_REPO=ghcr.io/org/repo \
VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo dev) \
COMMIT=$(git rev-parse --short HEAD) \
BUILDTIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
ko build \
--platform=linux/amd64,linux/arm64 \
--tags latest,"$VERSION" \
--bare ./cmd/web/
# Build locally without pushing (load into Docker)
ko build --platform=linux/amd64 --local --tags dev --bare ./cmd/web/
CI/CD pattern (GitHub Actions):
- uses: actions/setup-go@v5
- run: templ generate # if using Templ
- uses: ko-build/[email protected]
- env: { KO_DOCKER_REPO: ghcr.io/org/repo }
run: ko build --platform=linux/amd64,linux/arm64 --tags latest,${{ steps.version.outputs.version }} --bare ./cmd/web/
Required .ko.yaml:
defaultBaseImage: gcr.io/distroless/static-debian12:nonroot
builds:
- id: my-app
main: ./cmd/web
ldflags:
- -w
- -X main.Version={{.Env.VERSION}}
- -X main.CommitHash={{.Env.COMMIT}}
- -X main.BuildTime={{.Env.BUILDTIME}}
When NOT to use ko: The project uses CGo, needs OS packages at runtime,
or third-party binaries. In those cases, fall back to a multi-stage Dockerfile
with --mount=type=cache for Go module/build cache:
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /app/my-app ./cmd/web/
If no container infrastructure exists and ko is not an option, perform these steps:
# 1. Run templ generate if the project uses Templ
templ generate
# 2. Kill any process currently holding the port
lsof -ti:<PORT> | xargs kill -9 2>/dev/null || true
sleep 1
# 3. Build with version metadata
go build \
-ldflags="-w \
-X main.Version=$(git describe --tags --abbrev=0 2>/dev/null || echo dev) \
-X main.CommitHash=$(git rev-parse --short HEAD 2>/dev/null || echo unknown) \
-X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o ./app ./cmd/web
# 4. Start detached
nohup ./app > server.log 2>&1 &
echo "PID: $!"
If the project does not expose main.Version, main.CommitHash, or main.BuildTime,
drop the corresponding -X flags.
| Tool | Context | Dockerfile needed | Multi-arch | Watches files | Long-running |
|---|---|---|---|---|---|
| air | Local dev only | ❌ | ❌ | ✅ | No — terminal session |
| ko | Containerized prod | ❌ | ✅ (native) | ❌ | Yes — orchestrated |
| go build + nohup | Bare-metal/VM | ❌ | ❌ | ❌ | Yes — nohup |
| Docker multi-stage | Legacy (needs CGo/OS pkgs) | ✅ | ❌ (slow QEMU) | ❌ | Yes — orchestrated |
defer 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 ./...
Use golangci-lint with the recommended linter set below. If golangci-lint is not installed, install it first:
# Install (latest version, no pinning)
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
.golangci.ymllinters:
enable:
- errcheck # unchecked errors
- govet # suspicious constructs (shadow, printf misuse, etc.)
- staticcheck # comprehensive static analysis (SA*, S*, QF*)
- gosec # security anti-patterns (hardcoded creds, weak crypto, etc.)
- revive # idiomatic style (golint successor)
- gocritic # opinionated but high-signal checks
- ineffassign # assignments whose value is never used
- unused # unexported identifiers never referenced
- goimports # import ordering and formatting
- noctx # http requests without context.Context
linters-settings:
gosec:
severity: medium
revive:
rules:
- name: exported
- name: var-naming
- name: error-return
- name: context-as-argument # ctx must be first param
- name: context-keys-type # typed context keys only
golangci-lint run ./...
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
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.