| name | writing-go-code |
| description | Use when writing, modifying, or reviewing .go files, implementing new Go functions or packages, or when Go code style and conventions are relevant. |
Writing Go Code
Style Priorities (in order)
- Clarity -- purpose and rationale clear to the reader
- Simplicity -- simplest way to accomplish the goal
- Concision -- high signal-to-noise ratio
- Maintainability -- easy to modify correctly
- Consistency -- consistent with surrounding codebase
Core Rules
- Follow Google Go Style Guide
- Format all code with
golangci-lint fmt
- MixedCaps/mixedCaps only -- never snake_case (even constants:
MaxLength not MAX_LENGTH)
- No fixed line length -- refactor long lines instead of splitting
- Shorter names in Go than other languages; context reduces need for verbosity
- Comments explain why, not what
- Allow code to speak for itself with self-describing symbol names rather than redundant comments
- Use least-powerful mechanism: language primitive > stdlib > external dependency
Formatting
- Imports grouped: stdlib, external, internal (blank line between groups)
- Avoid magic numbers -- use named constants
- No unnecessary levels of abstraction
Naming
- Exported:
PascalCase
- Unexported:
camelCase
- Short receiver names (1-2 chars matching type initial)
- Acronyms keep case:
HTTPClient, xmlParser
- Package names: short, lowercase, no underscores, no
util/common/base
- Names should not feel repetitive when used:
queue.New() not queue.NewQueue()
- Predictable names -- a user should be able to predict the name in a given context
Function Design
- Keep functions small and focused
- Prefer composition over embedding
- Use functional options pattern for flexible constructors
- Return concrete types, accept interfaces
- Avoid
init() unless absolutely necessary
- Avoid variable shadowing
- Do not over-nest control flow (flatten with early returns)
Testing
- Table-driven tests where appropriate
- Name tests consistently:
TestXxx, BenchmarkXxx
- Use
t.Helper() in helper functions
- Use
t.Context() rather than context.Background() or context.TODO()
- Avoid global state in tests
- Tests should provide clear, actionable diagnostics on failure
Linting and Formatting (MANDATORY)
After every change to .go files:
- Run
dev/lint-fix — runs golangci-lint with --fix to auto-format and fix lint issues
- Verify
golangci-lint run passes with 0 issues
Both must pass before the task is considered done. dev/lint-fix covers gofmt, gofumpt, golines, and all enabled linters.
Metrics
All metrics live in pkg/metrics/. The project uses Prometheus via github.com/prometheus/client_golang.
Adding a new metric
- Declare the metric variable (unexported) in the appropriate file:
api.go — API-layer metrics (connections, envelope rates, latency)
sync.go — node-to-node replication metrics
blockchain.go — chain interaction metrics
indexer.go, payer.go, dbmetrics.go, migrator.go — domain-specific
- Register it in
registerCollectors() in metrics.go
- Expose it via an exported
Emit* function in the same file
- Regenerate the metrics catalog:
dev/gen/metrics-catalog
Metric type guidance
| Use case | Type |
|---|
| Count of events (requests, envelopes, errors) | Counter — monotonically increasing, use _total suffix |
| Current state (open connections, queue depth) | Gauge — can go up and down |
| Duration / latency distribution | Histogram — use _seconds suffix, observe in seconds |
Naming conventions
- Prefix:
xmtp_<subsystem>_<name>_<type-suffix>
- Examples:
xmtp_api_outgoing_envelopes_total, xmtp_sync_messages_received_count
- Counter suffix:
_total or _counter
- Histogram suffix:
_seconds, _duration
- Gauge suffix:
_gauge or none
Emit function patterns
func EmitFoo() { fooTotal.Inc() }
func EmitFoos(n int) { fooTotal.Add(float64(n)) }
func EmitFooDuration(d time.Duration) { fooDuration.Observe(d.Seconds()) }
func EmitBarGauge(label string, v float64) {
barGauge.With(prometheus.Labels{"label": label}).Set(v)
}
Checklist for new metrics
Common Pitfalls
See these references for detailed guidance on frequent Go mistakes: