main.go— Entry point, creates rootcobra.Command, uses embed.FS for templates/staticcmd/— Command implementations using customwent-commandpattern (serve.go)app/kernel.go— DI kernel that creates and runs the fx.Appapp/container.go— FX module container with DI providersapp/build_specs.go— Build metadata (version, channel, build date)app/bootstrap/— Bootstrap providers:init.go— Initialization function, validates env, creates working dirs, initializes loggersmodule/loggers.go— Logger providers (AuditLogger, ErrorLogger)module/iris.go— Iris web framework provider (uses Django template engine, enables hot-reload in dev)
app/config/env.go— Env struct withLoad()andValidate()methodstemplates/— Embedded template files (banner.template)static/— Embedded static files.env.dist— Environment template file (must be copied to .env and customized)
Iris uses the Django template engine (.html.django extension). Static files are served from the embedded filesystem at /static.
Copy .env.dist to .env and configure:
| Variable | Default | Description |
|---|---|---|
ENV |
dev |
Environment (dev or prod) |
VAR_DIR |
var |
Base directory for runtime files |
HOST |
127.0.0.1 |
Server bind address |
PORT |
3096 |
Server port |
The Validate() method enforces ENV as dev or prod, and port range 1-65535.
- Iris v12 — Web framework
- Cobra — Root command framework (integrated via
went-command) - Uber FX — Dependency injection container
- went-clio — CLI output formatting (console input/output)
- went-logger — File logging (AuditLogger, ErrorLogger)
- went-command — Custom command pattern
- air — Auto-reload during development
- golangci-lint v2 — Linter aggregator
Commands implement command.Interface and use GetHeader() to define cobra metadata:
var _ command.Interface = (*ServeCmd)(nil)
type ServeCmd struct {
command.Base
}
func NewServeCmd() command.Interface { ... }
func (c *ServeCmd) GetHeader() command.Header {
return command.Header{
Use: "serve",
Short: "Run the web server",
Long: "...",
}
}
func (c *ServeCmd) Invoke() any {
return c.run
}
func (c *ServeCmd) run(...) error { ... }The Kernel wraps fx.App creation and execution:
kernel := app.NewKernel(EmbedFS, specs, clio)
kernel.Run(instance.Invoke())Providers in container.go wrap types into FX-injectable named types:
var Container = fx.Module(
"container",
fx.Provide(module.IrisProvider),
fx.Provide(module.AuditLoggerProvider),
fx.Provide(module.ErrorLoggerProvider),
fx.Provide(config.LoadEnv),
)config/env.go defines a single typed struct with Load() (reads from environment) and Validate() methods. It uses godotenv to load .env file. No raw os.Getenv calls outside this file.
The bootstrap initialization creates the following directories at startup:
var/logs/— Log files (audit.log, error.log)var/uploads/— Uploaded files
Both app/service/ and app/model/ follow the same rule: one package per domain. The package name defines the domain context; each filename describes the type of object it contains, never repeating the domain name.
app/service/
├── domain_1/
│ └── client.go # package domain_1
└── domain_2/
├── client.go # package domain_2
├── client_interface.go # package domain_2
└── loader.go # package domain_2
Typical filenames: client.go, client_interface.go, loader.go, resolver.go, cache.go, parser.go
app/model/
├── domain_1/
│ └── entity.go # package domain_1
└── domain_2/
├── entity.go # package domain_2
├── dto.go # package domain_2
├── request.go # package domain_2
└── response.go # package domain_2
Typical filenames: entity.go, dto.go, request.go, response.go, enum.go, event.go
Why: the domain lives in the package, the role lives in the filename.
github/github_client.go❌ →github/client.go✅ Imports stay self-documenting:domain2.Client,domain2.Loader,release.Entity.
Each file defines exactly one primary object (struct or interface). The filename must match the role of that object, as described in the layer conventions above.
Every object must have a constructor named New<ObjectName>. Its parameters are exclusively the dependencies to be injected. No logic, initialization, or side effects of any kind inside the constructor — it only assigns fields.
func NewKernel(embedFS embed.FS, specs *Specs, clio *clio.Clio) *Kernel {
return &Kernel{
embedFS,
specs,
clio,
}
}Function and method names must always be a verb or a verbNoun (load, fetchReleases, parseResponse). Names must be self-explanatory in isolation.
Context already provided by the receiver type must not be repeated in the method name:
// receiver is ElementManager
func (m *ElementManager) getAll() { ... } // ✅
func (m *ElementManager) getElements() { ... } // ❌ redundant
// receiver is ReleaseClient
func (c *ReleaseClient) fetch() { ... } // ✅
func (c *ReleaseClient) fetchRelease() { ... } // ❌ redundant- Config file:
.golangci.ymlin v2 format staticcheckenabled with exclusions:-ST1000(package comments)
- All builds use vendor mode:
GOFLAGS="-mod=vendor" - Production builds require
VERSIONandCHANNELenv vars to be set - Build artifacts are output to
build/ - Use
make build-dev,make build-staging, ormake build(production)