Go architect
My personal, ever-growing collection of AI skills for OpenCode and Claude Code. Enforces strict clean architecture and professional standards.
npx -y skills add ralvarezdev/ralvaskills --skill go-architectAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Go 1.26 architectural standards — memory-aligned structs, typed enums, interface design, goroutine safety, iterators, idiomatic errors, sqlx + //go:embed SQL pattern, go.work multi-module layout. Use when writing, reviewing, or scaffolding Go code.
SKILL.md
20.4 KB, as published. Nobody here has run it
Go Architecture & Domain Modeling
Targets Go 1.26. See STACK.md for pinned dependency versions.
1. Constants & Enums
- Enums: Custom types with
iotaand an implementedString()method. Avoid bare primitives. Use1 << iotafor bitmasks. - Errors: Export package-level sentinel errors (
var ErrNotFound = ...) forerrors.Is(). Useerrors.Jointo aggregate multiple errors. - Grouping: When a file declares multiple
type,const, orvarat 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. - File-level ordering: type → const → var → func (decorder's default order). Enforced via code review; linter checking disabled to avoid friction with tooling defaults.
- Magic values: a bare numeric or string literal used more than once becomes a named constant. Enforced by
mnd(numbers) andgoconst(strings) (see §14).
2. Structures & Memory
- Design: Explicit struct tags, standard audit fields, composition via embedding. Keep tag casing (
json,yaml) consistent across the codebase —tagliatellecatches drift, opt-in and scoped since legacy tags often can't be renamed without a breaking change (see §14). - Optimization: Order fields largest → smallest to minimize padding.
- Semantics: Pointers for mutation or mutex-protected state; values for small, immutable payloads.
- Validation:
Validate() errorfor structs taking external input; tag-based validation viago-playground/validator. - Ordering: Use
cmp(cmp.Compare[T],cmp.Or,cmp.Less) as the default ordering toolkit. - Typed models over maps: Prefer a defined struct to
map[string]any/map[string]interface{}for shaped data — the compiler catches typos and missing fields a map silently swallows.forbidigoflags the anti-pattern (see §14);exhaustruct(opt-in, scoped to specific structs) catches incomplete struct literals once you've made the switch.
3. Instantiation
- Constructors: Always provide
New...to initialize maps, slices, channels safely. - Configuration: Functional Options pattern for complex setup; never large config structs passed by value.
4. Interfaces
- Design: Define interfaces where they're used, not where implemented. Keep them small (1–3 methods) — enforced by
interfacebloat(see §14). - Signatures: Accept interfaces (for easy mocking); return concrete structs — enforced by
ireturn(see §14).
5. Stdlib defaults
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 forzap/zerologonly when slog's allocator perf is provably insufficient.errors—errors.Is,errors.As,errors.Join.net.JoinHostPort/net.SplitHostPort— idiomatic host:port assembly (enforced bynosprintfhostportlinter). Neverfmt.Sprintf("%s:%d", host, port).
6. Iterators (Go 1.23+)
- Range-over-func:
for x := range seq { ... }whereseqisiter.Seq[T]. - Return
iter.Seq[T]when: the sequence is large/unknown-size, consumer might short-circuit, or it's backed by an underlying cursor (DB pagination, HTTP pages). - Return
[]Twhen: the result is small, bounded, and the caller almost always wants the whole thing. - Anti-pattern: wrapping a
[]Tinslices.Valuesjust to look modern. Round-tripping back toslices.Collectis wasted ceremony.
7. Concurrency
- State: Embed
sync.RWMutexdirectly above the fields it protects.sync/atomicfor simple counters. - Context fields: Never embed
context.Contextin structs (containedctxlinter enforces this). Pass it as a parameter; contexts are designed as function arguments, not persistent state. - Duration: Use
time.Durationprimitives; never multiply durations (durationcheckcatchestime.Second * 60— usetime.Minuteinstead). - Context nesting: Avoid nested contexts in loops or closures (
fatcontextcatches this anti-pattern). - WaitGroup: Use
sync.WaitGroup.Go(fn)(Go 1.25+) — replacesAdd(1) / go func() { defer Done(); ... }()boilerplate. - Context: Pass
context.Contextas the first parameter for any blocking or I/O. Usesignal.NotifyContextfor shutdown; the cancellation cause now records which signal fired (Go 1.26). - Containers:
GOMAXPROCSrespects cgroup CPU limits since Go 1.25 — don't hardcode in containerized deploys.
8. Errors & Panics
- Checking:
errors.Asfor custom types;errors.Isfor sentinels. Never force type assertions on error results — useokcheck orerrors.As(forcetypeassertenforces this). - Aggregation:
errors.Join(errs...)when collecting multiple errors (validation, parallel fan-out). - Wrapping:
fmt.Errorf("op: %w", err)to add context while preserving the chain. When wrapping external package errors, usewrapcheckto verify proper context addition (prevents "swallowing" errors). - Panics: Restrict to initialization (
MustCompilestyle). Never as control flow.
9. Testing
- Patterns: Table-driven tests and black-box testing (
package mypkg_test). Parallelize tests witht.Parallel()— checked and enforced byparalleltestandtparallellinters. Avoid deprecated testing APIs (ioutil.TempDir→t.TempDir());usetestingenforces modern stdlib testing functions. - Helpers:
t.Helper()as the first line in any custom assertion — enforced bythelper. - Concurrent code: Use
testing/synctest(GA in Go 1.25) — virtualizes time, waits for goroutines to quiesce, makes async tests deterministic. - Library:
stretchr/testifyfor assertions/mocks/suites when stdlib alone is awkward. Usetestifylintto catch incorrect assertion patterns (assertvsrequire). - Integration tests against Docker dependencies:
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) ortesting.Short()check sogo test ./...stays fast by default.
10. Packages
- Naming: Short, lowercase, single-word. Never
util,common,helpers. - Layout: Organize by domain/feature, not by technical layer.
- Domain modeling & interface placement: See hexagonal-arch for ports/adapters and ddd-architect for aggregates/bounded contexts. For
go.workmulti-module layout, see §16.
11. Dependencies & Logging
- Injection: Pass dependencies via constructors. No global state, no package
init()setups. For larger graphs useuber-go/fxorgoogle/wire. - Config:
spf13/viperfor layered env + file + flag configuration. - Logging:
log/slog(stdlib). Structured, context-aware.zap/zerologonly when slog is provably the bottleneck — start with slog.
12. Database access — SQL files + //go:embed
Strong preference: raw SQL in .sql files, embedded at compile time, executed via jmoiron/sqlx. Avoids ORM magic, keeps queries auditable in git, gives editors full SQL syntax highlighting and linting.
//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
- Migrations:
golang-migrate— versioned up/down pairs inmigrations/. - Dynamic queries: If a query needs runtime composition (optional filters), still write the static fragments in
.sqlfiles and join them in Go. Avoid building SQL by string concatenation against user input — use parameter binding.
13. Generics
- Usage: Data structures and utility functions only. If an interface would serve, prefer it.
- Self-referential constraints (Go 1.26):
type Adder[A Adder[A]] interface { Add(A) A }is valid — useful for fluent-style domain types. - Type aliases with type params (Go 1.24):
type IntMap[V any] = map[int]V.
14. Tooling
- Lint:
golangci-lintv2 — a single binary that aggregatesstaticcheck,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.ymland setgoimports.local-prefixesto your module path.- Essential linters:
govet,staticcheck,unused,gocritic,revive,errorlint,gosec,misspell,sloglint,godoclint. - Code quality:
nolintlintvalidates that//nolintdirectives are specific and actually suppress something — enforces intentional, clean ignores.varnamelenenforces minimum variable name length (catchesx,ioutside loops; configuremax-distance: 5, min-name-length: 1).errnameensures error types follow convention (ErrFoo,FooError) — complements §1's sentinel error pattern.stylecheckenforces naming conventions across the codebase (e.g., exported/unexported consistency). - Idiomatic patterns:
preallocdetects slices that could pre-allocate capacity — idiomatic Go performance practice.reassignflags package variable reassignment in favor of const/new-var idiom.makezerocatches unintended zero-length slice declarations.predeclaredprevents shadowing Go builtins.looppointerdetects address-of-loop-variable bugs (memory safety).recvcheckensures method receiver types are consistent within an interface.dogsledflags excessive blank identifiers (e.g.,_, _, _, v := ...suggests destructuring instead).mirrorcatches bytes/strings type mismatches (e.g., comparing[]bytetostringidiomatically).exptostdreplacesgolang.org/x/exppackages with stdlib equivalents — use stdlib for idiomatic Go.modernizesuggests modern Go simplifications (range-over-int, slices API, etc.). - Duplication & magic values:
dupl(structural clones),goconst(repeated string literals),mnd(magic numbers) — all push toward named constants/enums (see §1). - Complexity & size:
nestif,gocognit(cognitive complexity),maintidx(maintainability index), plus revive'sfile-length-limitandargument-limitrules — catch functions/files that should be split before they rot. - Declaration structure:
decorderenforces file-leveltype → const → var → funcordering (decorder's default). Setdisable-dec-order-check: falsein your.golangci.ymlto enable (see §1).gochecknoinitsflagsinit()outright (see §11). - Interfaces & enums:
interfacebloatcaps interface method count,ireturnenforces concrete returns (see §4);exhaustiveforces enum-typedswitchstatements to cover every value;forbidigobansmap[string]interface{}/map[string]anyin 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 inassets/forbidigo-patterns.yml). - modernize (standalone linter, golangci-lint v1.57+): all checks on by default — catches
range n(Go 1.22+),slices.Sort,slices.Contains,maps.Keys,maps.Values, etc. If too noisy, constrain viamodernize.settings(e.g.,forRangeInts: trueonly). - intrange (standalone linter): explicitly enabled — flags
for i := 0; i < n; i++whererange nis cleaner. Lightweight alternative to themodernizecoverage. - copyloopvar / perfsprint: modernization nudges —
copyloopvarflags now-redundantx := xloop-variable copies (Go 1.22+ semantics),perfsprintsuggests faster alternatives tofmt.Sprintf/fmt.Errorf(e.g.strconv,err.Error()). Both low-noise, safe to enable by default. - Optional: Scoped Linters (enable when appropriate for your project):
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/yamlnaming). 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.
- Testing linters:
paralleltest: Detects tests missingt.Parallel()— encourages test suite parallelization for faster feedback.tparallel: Catches inappropriatet.Parallel()usage in subtests, preventing race conditions and serialization bugs.thelper: Ensures test helper functions callt.Helper()— prevents test output pollution and clarifies stack traces.testifylint: Best practices for testify assertions — flags incorrectassertvsrequireusage and other testify pitfalls (see §9).usetesting: Replaces deprecated testing helpers (e.g.,ioutil.TempDir→t.TempDir()) with modern stdlib equivalents.
- Correctness & safety patterns:
containedctx: Detectscontext.Contextembedded in structs — pass as parameter instead (see §7).durationcheck: Prevents duration multiplication bugs (e.g.,time.Second * 60vstime.Minute).forcetypeassert: Flags forced type assertions withoutokcheck — useerrors.Asor 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: Flagsgolang.org/x/exppackages — 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 tocopyloopvar+perfsprint.
- Template: The
assets/golangci.ymltemplate includes sane defaults for all recommended linters with tested configuration values forlinters.settings. Copy it to.golangci.ymland customize thresholds (e.g.,gocognit.maxfor cognitive complexity) to match your project's complexity profile.
- Essential linters:
- Doc-comment coverage (opt-in): the main config disables revive's
exported/package-commentsrules — doc coverage isn't part of the standard gate (see §15). Run it on demand with the secondary configassets/golangci.doccheck.yml:golangci-lint run -c golangci.doccheck.yml ./.... - Repo-level style guards (opt-in, not golangci-lint): two conventions golangci-lint has no linter for — no one-line function bodies, no decorative separator comments (
// ──,# ═══). 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 (seerepo-tooling-architect§4 for Task/just setup) rather than the main lint gate, so a legacy-codebase failure doesn't block unrelated CI. - Modernization: Run
go fixperiodically (revamped in Go 1.26 as push-button modernizers). It rewrites legacy patterns toward current stdlib APIs and respects//go:fix inlinedirectives for local refactors. Ongoing hygiene, not a one-off. - Format:
gofmt(built-in) plusgoimports(viagolangci-lint) — no other formatter, no debate. - Build/run:
go build,go test,go vet,go workfor multi-module repos. Stdlib only — avoid Make/Task wrappers unless multi-language CI demands it.
15. Documentation
- Format: Exported names get full-sentence comments starting with the identifier name. Include a package-level comment.
- Style: Weave parameter and return names into prose (no
@param). Document specific error conditions. - Focus: Explain why (business rules, edge cases) — code already shows what.
16. Multi-module workspaces (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.
- Module boundary is the real privacy/reuse unit. Once something is its own module (own
go.mod), nesting it under a directory namedinternal/adds nothing — Go'sinternal/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 furtherinternal/inside that module (internal/order/internal/wire/). pkg/*— each subdirectory its own module, for code genuinely shared by 2+ service modules (validation, observability, an RFC 7807 problem-detail mapper). Don't default to creatingpkg/; per hexagonal-arch §7, a shared seam is only real once two consumers need it.- Root-level
domain— only for value objects/types genuinely shared across bounded contexts (e.g. aMoneytype used by both Order and Payment). Most contexts should keep their domain private inside their own service module. A shared rootdomainmodule is a deliberate Shared Kernel (see ddd-architect's context-mapping table), not a default. internal/<service>— one bounded context = one module. Internally structured per hexagonal-arch: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 singleinternal/<service>module typically contains several Application Services inside itsapp/layer (e.g.PlaceOrderService).cmd/<service>vsapp/<service>— keep distinct rather than merging composition intocmd.cmd/<service>/main.gostays minimal: parse flags/env, wiresignal.NotifyContext, call intoapp, 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.- Cross-context calls never reach into another context's
domain/oradapters/. They go through that context's public API (HTTP/gRPC, since each service is a separate deployable in a multi-module workspace). The calling context defines its own ports shaped for what it needs (OrderCreator, notorder.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. - Full worked example — an order-owning service plus a checkout orchestrator service, with code — lives in RECIPES.md.
Canonical libraries
See STACK.md for the full pinned list — viper, validator, cobra/pflag, gin, sqlx, grpc, protobuf-go, testify, fx, golang-migrate, buf.