go-architect — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited go-architect (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Targets Go 1.27.1. See STACK.md for pinned dependency versions.
iota and an implemented String() method. Avoid bare primitives. Use 1 << iota for bitmasks.var ErrNotFound = ...) for errors.Is(). Use errors.Join to aggregate multiple errors.type, const, or var at package level, consolidate each kind into a single parenthesized block (type (...), const (...), var (...)) rather than repeating the keyword. Keep unrelated groups separated by a blank line inside the block.mnd (numbers) and goconst (strings) (see §14).json, yaml) consistent across the codebase — tagliatelle catches drift, opt-in and scoped since legacy tags often can't be renamed without a breaking change (see §14).Validate() error for structs taking external input; tag-based validation via go-playground/validator.cmp (cmp.Compare[T], cmp.Or, cmp.Less) as the default ordering toolkit.map[string]any/map[string]interface{} for shaped data — the compiler catches typos and missing fields a map silently swallows. forbidigo flags the anti-pattern (see §14); exhaustruct (opt-in, scoped to specific structs) catches incomplete struct literals once you've made the switch.New... to initialize maps, slices, channels safely.interfacebloat (see §14).ireturn (see §14).Prefer stdlib over hand-rolled or third-party when stdlib now covers it.
slices, maps — collection helpers (Sort, Contains, Index, Clone, Equal, Concat, etc.)cmp — Compare, Less, Or, Ordered — the default ordering toolkit.iter — iter.Seq[T], iter.Seq2[K,V] for sequence-shaped returns (see §6).log/slog — structured logging; default for new code. Reach for zap/zerolog only when slog's allocator perf is provably insufficient.errors — errors.Is, errors.As, errors.Join.net.JoinHostPort / net.SplitHostPort — idiomatic host:port assembly (enforced by nosprintfhostport linter). Never fmt.Sprintf("%s:%d", host, port).for x := range seq { ... } where seq is iter.Seq[T].[]T in slices.Values just to look modern. Round-tripping back to slices.Collect is wasted ceremony.sync.RWMutex directly above the fields it protects. sync/atomic for simple counters.context.Context in structs (containedctx linter enforces this). Pass it as a parameter; contexts are designed as function arguments, not persistent state.time.Duration primitives; never multiply durations (durationcheck catches time.Second * 60 — use time.Minute instead).fatcontext catches this anti-pattern).sync.WaitGroup.Go(fn) (Go 1.25+) — replaces Add(1) / go func() { defer Done(); ... }() boilerplate.context.Context as the first parameter for any blocking or I/O. Use signal.NotifyContext for shutdown; the cancellation cause now records which signal fired (Go 1.26).GOMAXPROCS respects cgroup CPU limits since Go 1.25 — don't hardcode in containerized deploys.errors.As for custom types; errors.Is for sentinels. Never force type assertions on error results — use ok check or errors.As (forcetypeassert enforces this).errors.Join(errs...) when collecting multiple errors (validation, parallel fan-out).fmt.Errorf("op: %w", err) to add context while preserving the chain. When wrapping external package errors, use wrapcheck to verify proper context addition (prevents "swallowing" errors).MustCompile style). Never as control flow.package mypkg_test). Parallelize tests with t.Parallel() — checked and enforced by paralleltest and tparallel linters. Avoid deprecated testing APIs (ioutil.TempDir → t.TempDir()); usetesting enforces modern stdlib testing functions.t.Helper() as the first line in any custom assertion — enforced by thelper.testing/synctest (GA in Go 1.25) — virtualizes time, waits for goroutines to quiesce, makes async tests deterministic.stretchr/testify for assertions/mocks/suites when stdlib alone is awkward. Use testifylint to catch incorrect assertion patterns (assert vs require).testcontainers-go — spins up real Postgres/Redis/Kafka/etc. containers per test run instead of mocking the driver or relying on a shared dev instance. Gate behind a build tag (e.g. //go:build integration) or testing.Short() check so go test ./... stays fast by default.util, common, helpers.net/http, go/types); the folder's navigability, not the count, is the trigger. See code-design-refactor §4.go.work multi-module layout, see §16.init() setups. For larger graphs use uber-go/fx or google/wire.New(deps...) and never imports fx/wire — doing so drags the container into every consumer. When a graph is large, the deployable owns the container (app/, internal/bootstrap, cmd/); libraries stay constructor-only. Optional courtesy for fx users: ship an fx.Module in a dedicated subpackage (<pkg>/fxmod) that groups the fx.Provides for the library's constructors — whoever uses fx includes it, whoever doesn't calls New by hand, and the library stays DI-agnostic.spf13/viper for layered env + file + flag configuration.log/slog (stdlib). Structured, context-aware. zap/zerolog only when slog is provably the bottleneck — start with slog.//go:embed (alternative)Default: `sqlc`. Schema-first, code-generated queries — write named queries in .sql files against your real migrated schema, run sqlc generate, get back typed Go row structs and query methods, checked at generate-time against the actual database. No ORM magic, no hand-written scanning code, queries stay plain auditable SQL in git.
-- query.sql
-- name: GetUserByID :one
SELECT * FROM users WHERE id = $1;# sqlc.yaml
version: "2"
sql:
- engine: "postgresql"
queries: "query.sql"
schema: "migrations"
gen:
go:
package: "db"
out: "db"func (r *UserRepo) GetByID(ctx context.Context, id int64) (db.User, error) {
return r.queries.GetUserByID(ctx, id)
}Layout:
internal/userrepo/
├── repo.go
├── sqlc.yaml
├── query.sql
└── db/ # generated: models.go, query.sql.go, db.go — never hand-editgoose or golang-migrate — versioned up/down pairs in migrations/. sqlc's schema: reads the same migration files, so schema drift between the two is impossible by construction.sqlc generate in CI (or a pre-commit hook) and fail the build if the generated tree differs from what's committed — generated code is committed, not built on the fly.Alternative: `jmoiron/sqlx` + `//go:embed`. Reach for this when a query needs genuine runtime composition sqlc's static named-query model can't express (e.g. an arbitrary number of optional filter clauses), or when a package deliberately avoids a codegen step. Write raw SQL in .sql files, embed at compile time, execute via sqlx's struct-tag scanning:
//go:embed queries/get_user_by_id.sql
var getUserByIDSQL string
func (r *UserRepo) GetByID(ctx context.Context, id int64) (User, error) {
var u User
err := r.db.GetContext(ctx, &u, getUserByIDSQL, id)
return u, err
}Layout:
internal/userrepo/
├── repo.go
└── queries/
├── get_user_by_id.sql
├── insert_user.sql
└── list_users.sql.sql files and join them in Go rather than building SQL by string concatenation against user input — always use parameter binding.type Adder[A Adder[A]] interface { Add(A) A } is valid — useful for fluent-style domain types.type IntMap[V any] = map[int]V.golangci-lint v2 — a single binary that aggregates staticcheck, govet, gofmt, goimports, revive, and dozens more. Pin via .golangci.yml (v2 config schema). Run on every commit and in CI. Treat warnings as errors. Drop-in template: assets/golangci.yml — copy to your project root as .golangci.yml and set goimports.local-prefixes to your module path.govet, staticcheck, unused, gocritic, revive, errorlint, gosec, misspell, sloglint, godoclint.nolintlint validates that //nolint directives are specific and actually suppress something — enforces intentional, clean ignores. varnamelen enforces minimum variable name length (catches x, i outside loops; configure max-distance: 5, min-name-length: 1). errname ensures error types follow convention (ErrFoo, FooError) — complements §1's sentinel error pattern.prealloc detects slices that could pre-allocate capacity — idiomatic Go performance practice. reassign flags package variable reassignment in favor of const/new-var idiom. makezero catches unintended zero-length slice declarations. predeclared prevents shadowing Go builtins. recvcheck ensures method receiver types are consistent within an interface. dogsled flags excessive blank identifiers (e.g., _, _, _, v := ... suggests destructuring instead). mirror catches bytes/strings type mismatches (e.g., comparing []byte to string idiomatically). exptostd replaces golang.org/x/exp packages with stdlib equivalents — use stdlib for idiomatic Go. modernize suggests modern Go simplifications (range-over-int, slices API, etc.).dupl (structural clones), goconst (repeated string literals), mnd (magic numbers) — all push toward named constants/enums (see §1).nestif, gocognit (cognitive complexity), maintidx (maintainability index), plus revive's file-length-limit and argument-limit rules — catch functions/files that should be split before they rot.decorder enforces file-level type → const → var → func ordering (decorder's default). Set disable-dec-order-check: false in your .golangci.yml to enable (see §1). gochecknoinits flags init() outright (see §11).interfacebloat caps interface method count, ireturn enforces concrete returns (see §4); exhaustive forces enum-typed switch statements to cover every value; forbidigo bans map[string]interface{}/map[string]any in favor of typed structs (see §2) — requires careful regex tuning for your codebase (consider enforcing struct typing via code review if regex patterns get unwieldy; template includes reference patterns in assets/forbidigo-patterns.yml).range n (Go 1.22+), slices.Sort, slices.Contains, maps.Keys, maps.Values, etc. If too noisy, constrain via modernize.settings (e.g., forRangeInts: true only).for i := 0; i < n; i++ where range n is cleaner. Lightweight alternative to the modernize coverage.copyloopvar flags now-redundant x := x loop-variable copies (Go 1.22+ semantics), perfsprint suggests faster alternatives to fmt.Sprintf/fmt.Errorf (e.g. strconv, err.Error()). Both low-noise, safe to enable by default.exhaustruct: Flags incomplete struct literals; scoped to config/DTO structs to manage noise. Enable once you've migrated to fully-typed structs (see §2).tagliatelle: Enforces struct-tag casing consistency (json/yaml naming). Scope to new code since legacy tags often can't be renamed without breaking changes; useful when refactoring APIs (see §2).depguard: Controls allowed imports (e.g., no test frameworks in prod code). Requires careful allowlist configuration per project structure — enable after establishing import boundaries.paralleltest: Detects tests missing t.Parallel() — encourages test suite parallelization for faster feedback.tparallel: Catches inappropriate t.Parallel() usage in subtests, preventing race conditions and serialization bugs.thelper: Ensures test helper functions call t.Helper() — prevents test output pollution and clarifies stack traces.testifylint: Best practices for testify assertions — flags incorrect assert vs require usage and other testify pitfalls (see §9).usetesting: Replaces deprecated testing helpers (e.g., ioutil.TempDir → t.TempDir()) with modern stdlib equivalents.containedctx: Detects context.Context embedded in structs — pass as parameter instead (see §7).durationcheck: Prevents duration multiplication bugs (e.g., time.Second * 60 vs time.Minute).forcetypeassert: Flags forced type assertions without ok check — use errors.As or check the boolean (see §8).fatcontext: Detects nested contexts in loops/closures — anti-pattern that escapes context cancellation.mirror: Catches bytes/strings type mismatches; encourages idiomatic type usage.exptostd: Flags golang.org/x/exp packages — use stdlib equivalents instead (core idiom).wrapcheck: Ensures external package errors are wrapped with context; prevents "swallowing" errors (see §8).unparam: Reports unused function parameters — aids cleanup (can be noisy during refactoring).funcorder: Enforces consistent function/method/constructor ordering within types.modernize: Suggests modern Go simplifications (range-over-int, slices API, maps functions, etc.) — complement to copyloopvar + perfsprint.assets/golangci.yml template includes sane defaults for all recommended linters with tested configuration values for linters.settings. Copy it to .golangci.yml and customize thresholds (e.g., gocognit.max for cognitive complexity) to match your project's complexity profile.unused only flags unused unexported symbols, so an exported function no main reaches passes the lint gate forever. Run deadcode -test ./... (golang.org/x/tools/cmd/deadcode) so tests count as roots — without -test every test helper looks dead. It does not follow dynamic dispatch (cobra RunE, handler funcs stored in maps), so confirm each hit before deleting. Re-run after a deletion pass: removing one function can be the last use of another. Wire it into its own task target rather than the lint gate, since it needs a main to root from. For a shared library with several consumers, run it per consumer with -filter and cross-check with go list -deps; see codebase-hygiene-reviewer.exported/package-comments rules — doc coverage isn't part of the standard gate (see §15). Run it on demand with the secondary config assets/golangci.doccheck.yml: golangci-lint run -c golangci.doccheck.yml ./....// ──, # ═══). Ship as shell scripts, not lint rules, since existing codebases likely have violations needing a separate cleanup pass first: assets/check-no-oneline-functions.sh, assets/check-no-separator-comments.sh. Wire each into its own task-runner target (see repo-tooling-architect §4 for Task/just setup) rather than the main lint gate, so a legacy-codebase failure doesn't block unrelated CI.go fix periodically (revamped in Go 1.26 as push-button modernizers). It rewrites legacy patterns toward current stdlib APIs and respects //go:fix inline directives for local refactors. Ongoing hygiene, not a one-off.gofmt (built-in) plus goimports (via golangci-lint) — no other formatter, no debate.go build, go test, go vet, go work for multi-module repos. Stdlib only — avoid Make/Task wrappers unless multi-language CI demands it.@param). Document specific error conditions.go.work)For repos where go.work ties together independently-versioned modules — one per bounded context, plus shared libraries — rather than a single go.mod.
go.mod), nesting it under a directory named internal/ adds nothing — Go's internal/ import restriction is a single-module-tree mechanism and does nothing extra for something already module-isolated. Treat the directory name as organizational labeling, not enforcement. If a service module needs a genuine internal-only boundary, nest a further internal/ inside that module (internal/order/internal/wire/).** — each subdirectory its own module, for code genuinely shared by 2+ service modules (validation, observability, an RFC 9457 problem-detail mapper). Don't default to creating pkg/`; per hexagonal-arch §7, a shared seam is only real once two consumers need it.Money type used by both Order and Payment). Most contexts should keep their domain private inside their own service module. A shared root domain module is a deliberate Shared Kernel (see ddd-architect's context-mapping table), not a default.domain/, app/, adapters/{primary,secondary} — or flatter, since hexagonal-arch mandates the dependency direction, not the folder split. "Service" here means the deployable module itself, not ddd-architect's Domain/Application Service patterns — a single internal/<service> module typically contains several Application Services inside its app/ layer (e.g. PlaceOrderService).cmd. cmd/<service>/main.go stays minimal: parse flags/env, wire signal.NotifyContext, call into app, handle the exit code. app/<service> is the actual composition root — DI graph, adapter wiring, server lifecycle — so it's exercisable independently of process-level concerns.OrderCreator, not order.Repository) — the same "define interfaces where used" rule from §4, applied across a context boundary. This is the Go-flavored version of ddd-architect's Customer/Supplier or Open Host Service patterns.§16 is the default. For a repo that has outgrown it, there is no single correct layout — only better fits for a project's shape. LAYOUTS.md is the catalogue (library, flat by concept, feature-first, escalated, grouped at the root, multi-module, layer-first), each with its trade-off, plus sections on module count, process types, generated code, cross-context communication, tests, ownership and layout smells, and a per-project review checklist. Review it per project and re-review when contexts, binaries or teams change.
Principles that hold across them:
core/ + use cases + adapters/<tech>/ only when a feature is domain-heavy.**/ path patterns, record exceptions.contexts/) past ~7; extract a group as a module past ~8–10 or with its own team.See STACK.md for the full pinned list — viper, validator, cobra/pflag, gin, sqlx, grpc, protobuf-go, testify, fx, golang-migrate, buf.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.