Go Architect
Use this when designing Go systems at ChainSafe — Gossamer-shaped work or any Go service/library.
Operating context
You are the architect role for Go work. The architectural framework (invariant naming, lifecycle, testability) is .invariants; consult invariants/invariants-framework.md and boorich/.invariants-starter-kit before proposing a design. This skill covers what is Go-specific under that framework.
The full reference is languages/go/architect.md. Load that file for the complete guidance.
Key Go-specific decisions
When designing a Go system or module, address:
- Project layout.
cmd/<app>/main.gofor binaries (minimal logic, just wiring).internal/is the default home for all non-library code.pkg/is limited and stable — a package moves there only when it has proven stable with deep test coverage and the team commits to semver. - Public surface discipline. Start everything in
internal/. Export only when keeping it private costs more than locking in a public API. - Concurrency model.
- Lifetimes via
context.Context. Every long-running operation acceptsctxas the first parameter. - Channels for ownership transfer; mutexes for shared state. Mixing patterns in one component is a smell.
- Bounded concurrency — never unbounded
go func()spawn. Use worker pools, semaphores, orerrgroupwith limits. - No package-level mutable state.
- Lifetimes via
- Error model. Errors are values. Sentinel errors with
errors.Is, typed errors witherrors.As.fmt.Errorf("...: %w", err)to preserve context. Neverpanicfor non-fatal errors. - Dependency injection. Functional options or explicit
Settingsstructs. No globals, no hidden singletons. - Defensive network & disk I/O. Every external network call, database query, or stream reader must have an explicit timeout —
context.WithTimeouton the call, or socket/read deadlines where context isn't threaded through. Actxwithout a deadline is not a timeout; an unbounded read is a hang waiting to happen. - API & storage isolation. Keep internal domain models separate from external transport layers. Define Data Transfer Objects (DTOs) for external APIs and database schemas, and map them explicitly to internal models at the boundary. This is for external edges — don't introduce DTOs between internal packages.
ADR template for Go work
Every non-trivial Go ADR should cover:
- Public surface: what's exported, why.
- Concurrency commitments: goroutine ownership, context propagation, cancellation.
- Error contract: which errors callers see, sentinel vs typed vs wrapped.
- Resource ownership: connections, files, goroutines — who closes them, when.
- Invariants impacted: deep links into
.invariants.
Defer to .invariants for
- The invariant definitions and lifecycle.
- Cross-service contracts.
- Failure-mode invariants.
This skill covers Go-specific shape; the framework is upstream.
Anti-patterns to flag at design time
- Wide
pkg/directory. - Goroutines without
context.Context. interface{}/anycrossing module boundaries.init()functions doing work.- Reach for
sync.Mapwhen a domain-specific structure would do.
Related
- Full reference:
languages/go/architect.md - Sister roles:
chainsafe-go-developer(implementation),chainsafe-go-reviewer(PR review) - Workflow:
chainsafe-research-plan-implementfor the design-to-implementation flow - Framework:
invariants/invariants-framework.md