go-service

Gopher CircleCI codecov Go Report Card Go Reference Stability: Active

🧰 Go Service

github.com/alexfalkowski/go-service/v2 is an opinionated framework/library for building Go services with consistent wiring for configuration, DI, transports, telemetry, crypto, etc.

This repo is primarily a library of packages (no top-level cmd/ binary). Services built on top typically define their own main package elsewhere and import this module.

Long-running services are expected to start from go-service-template, while short-lived client commands start from go-client-template. Both compose the high-level module bundles from this repository. These are the primary supported paths. Lower-level package-by-package composition is still available, but it is an advanced mode and may require extra manual registration.


πŸš€ Install

For a new long-running service, start from go-service-template so the application main, server command wiring, configuration fixtures, and standard module composition are generated together. For a short-lived control, migration, or batch command, start from go-client-template; it demonstrates cli.Application.AddClient, module.Client, and lifecycle OnStart command work.

For direct package use in an existing module, add the library dependency with the versioned module path:

go get github.com/alexfalkowski/go-service/v2

Use the Go version declared in go.mod or newer when installing or building this module.


🧩 Dependency Injection (Fx)

The framework is designed around dependency injection and uses Uber Fx (and Dig under the hood). Most subsystems expose Fx modules that you compose into your service.

If you are new to Fx, their docs/examples are worth reading first.

Module bundles

The module package exposes three top-level bundles:

These bundles are the intended default for services generated from go-service-template. They handle the internal registration expected by the framework so most services do not need to wire lower-level transport or lifecycle helpers manually.

Minimal CLI bootstrap example

This repository is a library, so your binary is usually in another module. A typical main uses cli.Application and composes module bundles:

package main

import (
    "github.com/alexfalkowski/go-service/v2/cli"
    "github.com/alexfalkowski/go-service/v2/context"
    "github.com/alexfalkowski/go-service/v2/module"
    "github.com/alexfalkowski/go-service/v2/os"
)

func main() {
    app := cli.NewApplication(func(commander cli.Commander) {
        serve := commander.AddServer("serve", "Run the service", module.Server)
        serve.AddConfig("file:./config.yaml") // adds the `-config` / `-c` config flag with this default
    })

    os.Exit(app.RunCode(context.Background()))
}

The file:./config.yaml default above expects a non-empty config file. A minimal server config can start with the environment plus one enabled transport:

environment: development
transport:
  http:
    address: tcp://localhost:8000
    timeout: 10s

Use app.RunCode(context.Background()) from main when exiting the process. It returns os.ExitCodeSuccess on success, returns a requested non-zero shutdown exit code such as os.ExitCodeServeFailure, and returns os.ExitCodeFailure for other errors. Use app.Run(context.Background()) in tests or embedding code that needs to inspect the returned error.


πŸ–₯️ CLI

Services commonly expose two command shapes:

The framework uses acmd. Your service’s main typically wires Fx modules + commands.

This repo intentionally does not ship a ready-to-run main β€” it provides the building blocks. In normal usage server applications consume them through go-service-template plus module.Server, while short-lived commands use go-client-template plus module.Client, rather than wiring every subsystem manually.


πŸ—‚οΈ Repository layout

The repo is intentionally split between high-level service composition and lower-level reusable helpers:

As a rule of thumb: if you want protocol primitives or shared helpers, start in net/...; if you want service wiring and middleware policy, start in transport/.... Shared metadata, header, and lifecycle helpers live under net/..., including net/http/meta, net/grpc/meta, net/header, and net/server.Register.

For most service authors, the right starting point is still the high-level module bundles rather than these lower-level packages directly.


βš™οΈ Configuration

Supported config formats

The config decoder supports:

Selecting the config source (-config / -c flags)

Config input is routed by flags called -config and -c:

[!IMPORTANT] Because the user config directory is part of that search, runtimes using default lookup are expected to provide HOME or XDG_CONFIG_HOME. Services that cannot rely on those environment variables should pass an explicit -config file:<path> or -config env:<ENV_VAR> source.

Typed decoding and validation

At runtime, services typically decode into a struct (often embedding config.Config) and validate it using go-playground/validator.

The library provides a helper config.NewConfig[T] which:

Empty detection uses zero-value semantics and supports config types containing maps, slices, or other non-comparable fields.

Example:

type WorkerConfig struct {
    Queue string `yaml:"queue" json:"queue" toml:"queue" validate:"required"`
}

type AppConfig struct {
    Worker         *WorkerConfig `yaml:"worker" json:"worker" toml:"worker" validate:"required"`
    *config.Config `yaml:",inline" json:",inline" toml:",inline" validate:"required"`
}

func loadConfig(decoder config.Decoder, validator *config.Validator) (*AppConfig, error) {
    return config.NewConfig[AppConfig](decoder, validator)
}

func sharedConfig(cfg *AppConfig) *config.Config {
    return cfg.Config
}

func workerConfig(cfg *AppConfig) *WorkerConfig {
    return cfg.Worker
}

var AppConfigModule = di.Module(
    di.Constructor(config.NewConfig[AppConfig]),
    di.Decorate(sharedConfig),
    di.Constructor(workerConfig),
)

Compose AppConfigModule alongside module.Server or module.Client. The decorator projects the embedded shared *config.Config into the standard graph, so the service-specific config is decoded once while existing transport, SQL, and telemetry projections continue to work. Add constructors like workerConfig for service-owned sub-configs.

The standard top-level config shape

The canonical top-level config type is config.Config (in config/config.go). It contains:

Most sub-configs are optional pointers. Conventionally, nil means disabled.


πŸ” Source strings (secrets, DSNs, paths)

Many fields accept a source string rather than only a literal:

This is used for secrets and key material (TLS keys, HMAC keys, webhook secrets, SQL DSNs, etc). env: values and literal values are returned exactly as provided; they are not trimmed.

Example:

hooks:
  key: current
  secrets:
    current: env:WEBHOOK_SECRET

🌍 Environment

Top-level environment is:

environment: development

This is an env.Environment value used to drive environment-specific behavior in services.


πŸ—œοΈ Compression

Compression kinds used by subsystems that support compression:


🧾 Encoders

Encoding kinds used by subsystems that support encoding. encoding.Map registers each encoder under exactly one canonical kind (no aliases):

[!NOTE]


πŸ’Ύ Cache

Cache configuration is defined in cache/config.Config. Built-in driver kinds are redis and ttlcache.

See docs/cache.md for the config shape, driver semantics, size/entry limits, key-namespace implications of compressor/encoder, and Cache.Flush behavior.


🚩 Feature flags (OpenFeature)

feature.Config embeds client-side config (config/client.Config): address, timeout, retry, breaker, limiter, TLS, token, and options. This repository does not construct a built-in OpenFeature provider from this config β€” supply your own openfeature.FeatureProvider and feature.Module registers it with the SDK lifecycle.

See docs/feature-flags.md for a full config example and provider-wiring notes.


πŸͺ Webhooks (Standard Webhooks)

Configured via hooks.Config, using Standard Webhooks signing/verification with key rotation and an optional clock-skew leeway.

See docs/webhooks.md for the config shape, signing/verification behavior, leeway, idempotency guidance, and the CloudEvents structured-encoding requirement.


πŸ†” ID generation

Supported ID kinds:

Config:

id:
  kind: uuid

[!NOTE] ID generators produce operational identifiers such as request ids, webhook ids, and token jti values. They are not a secret-material API and should not be used as passwords, bearer tokens, or other credentials. Omit id entirely to select the uuid default. If id is present, kind must be one of the supported registered kinds. Sortable kinds such as ksuid, ulid, and xid expose ordering characteristics.


πŸš€ Runtime enhancements

Server commands created through cli.Application.AddServer include runtime.Module, which currently enables:

[!NOTE] This registration is best-effort and does not fail startup if a memory limit cannot be applied. When automemlimit detects an unlimited cgroup and GOMEMLIMIT is not already configured, it sets Go’s runtime memory limit to math.MaxInt64, replacing any programmatic limit applied before startup. Direct Fx compositions and client-style commands should include runtime.Module explicitly when they want this behavior.


🐘 SQL (Postgres)

SQL root config is database/sql.Config, with Postgres under sql.pg. module.Server and module.Client both wire PostgreSQL support via database/sql/pg.Module; a nil sql or sql.pg block disables it.

See docs/sql.md for the config shape, reader/writer pool settings, DSN resolution, ping/health-check guidance, and dependencies.


🩺 Health

Health checks are based on go-health and expose Kubernetes-style /<name>/healthz, /<name>/livez, and /<name>/readyz endpoints, plus the standard gRPC grpc.health.v1.Health service when gRPC transport is enabled.

module.Server installs the HTTP/gRPC health transports, but services own the checks and observer mapping β€” see the executable Registrations example.

See docs/health.md for endpoint behavior, checker wiring, and the gRPC health protocol.


πŸ“‘ Telemetry

Telemetry config root is telemetry.Config, covering resource attributes, metadata size limits, propagation formats, and the logger/metrics/tracer signals.

See docs/telemetry.md for the config shape, metadata, propagation, logging (JSON/text/tint/OTLP), metrics (Prometheus/OTLP, histogram buckets), tracing, libraries used, and dependencies.


🎫 Tokens

Token configuration is rooted at token.Config, usually nested under transport.http.token and/or transport.grpc.token. Supported kinds are jwt and paseto, plus a shared Casbin-based access-control layer configured at transport.access.

See docs/tokens.md for Casbin RBAC access control, and the JWT/PASETO config shapes, key rotation, and verification semantics.


🚦 Limiter

Limiter config is transport/limiter.Config, typically applied at transport level. Built-in key kinds are user-id, transport-service-method, service-method, ip, and user-agent.

See docs/limiter.md for the config shape, defaults, key semantics, and response headers/metadata.


πŸ•’ Time (network time)

Time config:

time:
  kind: nts
  address: time.cloudflare.com
  timeout: 2s

Supported kinds:

Omit the time block to disable network time. If the block is present, kind must be ntp or nts; empty or unknown kinds fail startup with the time provider not found error. address is provider-specific and is used when the network time provider performs I/O. timeout bounds network operations for the selected provider; a zero value uses the upstream client’s default timeout, and negative values are invalid.


🌐 Transport

The transport layer provides higher-level wiring and middleware policy for communication in/out of the service: composed HTTP/gRPC server and client stacks, retries, breakers, token middleware, and health wiring. net/... holds the lower-level protocol primitives those stacks build on.

Supported stacks include gRPC, HTTP REST/RPC abstractions with content negotiation, HTTP MVC helpers, and CloudEvents (transport/http/events).

See docs/transport.md for server configuration and TLS, HTTP content types and streaming (NDJSON), route policy, route-miss and MVC error handling, forwarded-IP/reflection posture, and client-side circuit breakers and retries.


πŸ”‘ Cryptography

The crypto root config is crypto.Config and supports AES, Ed25519, HMAC, and RSA key types. Most fields are source strings.

See docs/crypto.md for the config shape, key format requirements, the crypto.Message encryption API, and dependencies.


πŸ› οΈ Debug endpoints

Debug server config exposes statsviz, pprof, and fgprof under /<name>/debug/..., optionally behind TLS.

See docs/debug.md for the config shape, endpoint paths, and TLS setup.


πŸ§‘β€πŸ’» Development

This repo generally follows the Uber Go Style Guide and uses a bin/ git submodule for make targets; run make help to discover them.

See docs/development.md for repo setup, development dependencies, tests, lint/format, security checks, benchmarks, fuzz tests, coverage reports, code generation, and architecture diagrams.