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.
_ in security-sensitive or production paths.cali-coding-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 source files, 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
Use this policy for every new Go project unless the user explicitly chooses a different local dev workflow:
cali-coding-go-standards is the single source of truth for Air policy.cali-coding-go-stack must not duplicate the Air policy; it may only reference
cali-coding-go-standards.AGENTS.md must list air as the default local dev command.Makefile dev must start Air..air.toml when it is the project's standard dev config..air.toml.local for user-specific overrides..air.toml.example only when .air.toml is intentionally local and not
committed.solo.yml (air and make dev are the same
workflow)..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", "yaml", "css", "js"]
exclude_dir = ["tmp", "vendor", "node_modules", "testdata"]
exclude_regex = ["_templ\\.go$"] # prevents rebuild loop from generated files
kill_delay = "500ms"
send_interrupt = true
stop_on_error = true
[log]
time = true
[misc]
clean_on_exit = true
For projects without Templ, remove the pre_cmd line entirely. For projects
using Go tool dependencies, go tool templ generate is also acceptable.
.gitignoreAlways ignore Air's compiled binary output and local overrides:
tmp/
.air.toml.local
Do not ignore .air.toml when it is the committed project config.
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.
.templ changes are handled by templ generate in Air pre_cmd.go build + ./app manually during a dev session.pgrep -x air
air or make dev before testing the change.Never use Air outside of local development. For deploying or restarting the app in staging or production, follow this priority order:
If the project has a deploy mechanism — a shell script, a Makefile target,
a Dockerfile, or a process manager config — use it. Do not improvise.
If no deploy mechanism exists, perform these steps in order:
# 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 | Builds with ldflags | Watches files | Long-running process |
|---|---|---|---|---|
| air | Local dev only | ❌ (uses dev) | ✅ | No — terminal session |
| deploy mechanism / steps above | Staging / production | ✅ | ❌ | Yes — nohup |
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
development
PocketBase v0.39+ development - API rules, auth, collections, SDK, realtime, files, Go/JS extending, deployment, production tuning.
tools
Auto-initialize structured documentation for any project using lat.md (knowledge graph of markdown files with [[wiki links]], // @lat: code refs, and semantic search). Detects cali-product-workflow artifacts (spec-product.md, spec-tech.md, critiques) and uses them as seed material. Falls back to extracting business rules, architecture, and design decisions directly from the codebase. Use when a project lacks structured documentation or when lat.md/ is missing. After seeding, lat.md extension hooks keep documentation alive automatically.
testing
[Cali] Server security audit and hardening for private servers behind Tailscale. Use when: auditing server security, hardening SSH/firewall/Docker, checking for vulnerabilities, setting up fail2ban, reviewing port exposure, or responding to security alerts. Covers 6 layers: CloudFlare, UFW, Tailscale, SSH, Docker, Application. Triggers: "server security", "security audit", "harden server", "SSH hardening", "firewall rules", "UFW config", "fail2ban", "port security", "Docker security", "vulnerability check", "security review".
tools
Run supply chain security scans before installing packages or before releases. Triggers when: user installs a package (npm, pip, go get, brew), user asks to 'scan dependencies', 'check vulnerabilities', 'supply chain', 'security audit', 'run trivy', 'run socket', or before any release/deployment. Also triggers on mentions of: socket.dev, trivy, OSV-scanner, dotenvx, CVE, dependency audit. Covers all four tools with concrete commands.