A minimal Go microservice template with Cobra/Viper CLI wiring, ldflags-driven versioning, logrus logging, Makefile targets, tests, and GitHub Actions CI/CD (lint/test/build on PRs/main plus auto-tagged releases on main). The structure is intentionally simple so you can plug in your runtime workloads quickly.
- Requirements: Go 1.21+ (module sets 1.23/1.24), GNU
make. - Clone and create your branch:
git checkout -b feature/your-branch. - Build:
make build(binary./microservice-template). - Run:
make run(invokesgo run -race cmd/microservice-template.go serve). - Version:
./microservice-template --version. - Lint:
make lint(golangci-lint). - Test:
make testor single testgo test ./... -run TestName -count=1. - Generate + test:
make test-with-gen(runs proto + swagger generation first). - Generate + lint:
make lint-with-gen(runs proto + swagger generation first). - gRPC tests:
make test-grpc(runs gRPC package including integration). - HTTP tests:
make test-http(runs HTTP package tests). - Coverage:
make test-coverage(writescoverage.out). - Tidy deps:
make tidy; update deps:make update. - HTTP quickstart: see docs/HTTP_SWAGGER_GUIDE.md; enable with
HTTP_ENABLED=true, generate API withmake generate-api, test with curl. - gRPC quickstart: see docs/GRPC_GUIDE.md; enable with
GRPC_ENABLED=true, test with grpcurl; use shared protocols fromhttps://github.com/andskur/protocols-template.git.
- Command:
make rename NEW_NAME=my-service(required parameter). - Valid NEW_NAME: lowercase letters, numbers, hyphens, optional
/segments (e.g.,my-service,github.com/yourorg/my-service). - Updates: module path and imports, Makefile vars, entrypoint file, Cobra root
Use, swagger API struct name, Dockerfile binary, README/AGENTS references, optional git remote. - After rename regenerate generated code:
make generate-all(or at leastmake generate-api+ proto generation as needed). - Verify after rename:
go test ./...,make build,./<new-binary> --version.
- Simple, small footprint using standard libs plus Cobra/Viper/logrus.
- Module system for optional components (repository, service, HTTP, gRPC, queue, etc.).
- HTTP REST API with Swagger/OpenAPI spec-first approach using go-swagger.
- gRPC API with protocol buffer code generation and health checks.
- Version metadata injected via ldflags (
pkg/version). - Structured logging via
pkg/loggersingleton. - Makefile targets for build/run/lint/test/tidy/update/generate-api.
- CI pipeline: lint/test/build on PRs and
main; release pipeline auto-tags onmainand publishes a GitHub release (source-only). - Tests included for CLI wiring, config defaults, versioning, logger singleton, helpers, HTTP and gRPC modules.
- Rename-friendly: single placeholder name with automated
make renametarget.
go-microservice-template/
├── api/ # Swagger/OpenAPI specifications
├── cmd/ # CLI entry + commands
├── config/ # Viper defaults and scheme
├── db/migrations/ # Database migration files (golang-migrate)
├── docs/ # Additional guides (HTTP_SWAGGER_GUIDE, GRPC_GUIDE)
├── internal/
│ ├── application.go # App wiring + module registration
│ ├── grpc/ # gRPC module (server, interceptors)
│ ├── http/ # HTTP module (handlers, middleware, auth)
│ ├── module/ # Module interface/manager
│ ├── repository/ # Repository module (optional)
│ ├── service/ # Business logic module
│ └── models/ # Domain models/enums
├── pkg/ # Reusable packages (logger, version)
├── protocols/ # Protocol definitions pulled via subtree (no bundled example)
├── scripts/ # Automation scripts (rename)
├── .github/workflows/ # CI/CD pipelines
├── Dockerfile # Multi-stage container build
├── docker-compose.yml # Local stack (Postgres/app)
├── Makefile # Build/run/lint/test/proto/swagger targets
├── README.md, AGENTS.md # Docs and guidelines
└── go.mod, go.sum # Dependencies
This template uses a module-based architecture for optional components. Modules provide a standard lifecycle (Init → Start → Stop) and can be enabled/disabled via configuration.
The template includes configuration placeholders for common modules:
| Module | Purpose | Config Key | Status |
|---|---|---|---|
| Repository | Database-backed persistence (wraps DB connection) | database |
✅ Implemented (enabled when database.enabled is true) |
| Service | Business logic orchestrator (optional deps) | n/a | ✅ Implemented (always registered; repository optional) |
| HTTP | HTTP REST API server with Swagger/OpenAPI | http |
✅ Implemented (enabled when http.enabled is true) |
| gRPC Server | gRPC API server | grpc |
✅ Implemented (enabled when grpc.enabled is true) |
| gRPC Client | External service client for microservice communication | grpc_client |
✅ Implemented (enabled when grpc_client.enabled is true) |
| WebSocket | WebSocket server with pub/sub and rooms | websocket |
✅ Implemented (enabled when websocket.enabled is true) |
Configuration can come from env vars (recommended) or a config file (config.yaml is optional). Viper merges: flags > env vars > config file.
Env example (preferred):
export DATABASE_ENABLED=true
export DATABASE_DRIVER=postgres
export DATABASE_HOST=localhost
export DATABASE_PORT=5432config.yaml example (optional):
database:
enabled: true
driver: postgres
host: localhost
port: 5432
# ... other settings- The repository module registers only when
database.enabled(orDATABASE_ENABLED) istrue. - The service module always registers; if no repository is available, database operations return clear errors.
See config/scheme.go for configuration structure definitions.
The repository module requires PostgreSQL when enabled.
Quick start with Docker:
docker run --name postgres-dev \
-e POSTGRES_USER=dev \
-e POSTGRES_PASSWORD=dev \
-e POSTGRES_DB=microservice_dev \
-p 5432:5432 \
-d postgres:16-alpineInstall migration tool (one-time):
make migrate-installRun migrations:
# Apply all pending migrations
make migrate-up
# Check current migration version
make migrate-versionAvailable migration targets:
make migrate-install # Install golang-migrate CLI
make migrate-create # Create new migration (requires NAME=)
make migrate-up # Apply all pending migrations
make migrate-down # Rollback last migration
make migrate-force # Force migration version (requires VERSION=)
make migrate-version # Show current migration version
make migrate-drop # Drop all tables (⚠️ DANGER - requires confirmation)Local development with Docker Compose:
# Start Postgres and auto-run migrations (uses db/migrations)
make compose-up
# Stop services
make compose-down
# Restart services
make compose-restartFor production deployments, run migrations before starting the application or use a separate migration job in your deployment pipeline.
The HTTP module provides a REST API with Swagger/OpenAPI specification support using go-swagger.
Install go-swagger (one-time):
make swagger-installGenerate API server code from spec:
# Validate the swagger spec
make swagger-validate
# Generate server code from api/swagger.yaml
make generate-apiEnable HTTP module:
export HTTP_ENABLED=true
export HTTP_HOST=0.0.0.0
export HTTP_PORT=8080
export HTTP_MOCK_AUTH=true # For local development (bypasses JWT validation)
# Run the service
make runTest the HTTP endpoints:
# Health check (public endpoint)
curl http://localhost:8080/health
# Get user by email (requires auth in production, mock mode for dev)
curl -H "Authorization: Bearer test-token" \
"http://localhost:8080/users?email=test@example.com"Available HTTP targets:
make swagger-install # Install go-swagger CLI
make swagger-validate # Validate swagger spec
make generate-api # Generate server code from api/swagger.yaml
make swagger-clean # Remove generated code
make test-http # Run HTTP module testsConfiguration options:
http.enabled- Enable/disable HTTP server (default: false)http.host- Server host (default: 0.0.0.0)http.port- Server port (default: 8080)http.mock_auth- Use mock authentication for development (default: false)http.cors.enabled- Enable CORS (default: true)http.rate_limit.enabled- Enable rate limiting (default: false)http.rate_limit.requests_per_sec- Rate limit (default: 100.0)
For detailed HTTP development guide including adding new endpoints, authentication, and middleware, see docs/HTTP_SWAGGER_GUIDE.md.
The gRPC client module enables communication with external gRPC microservices. HTTP handlers use the client to fetch data from external services, making this template ideal for gateway/BFF (Backend for Frontend) patterns.
Architecture Overview:
- Hybrid approach: Pure gRPC client in
pkg/userservice/(proto types only) + module wrapper ininternal/grpcclient/(conversions + lifecycle) - HTTP integration: HTTP handlers receive grpcClient as dependency and use it to fetch from external services
- Service independence: Service layer remains focused on local business logic only
Enable gRPC client:
export GRPC_CLIENT_ENABLED=true
export GRPC_CLIENT_ADDRESS="user-service:9090"
export GRPC_CLIENT_TIMEOUT="30s"
# Run the service
make runConfiguration options:
grpc_client.enabled- Enable/disable client (default: false)grpc_client.address- External service address (default: "localhost:9090")grpc_client.timeout- Request timeout (default: "30s")grpc_client.keep_alive.time- Keep-alive ping interval (default: "10s")grpc_client.keep_alive.timeout- Keep-alive timeout (default: "1s")grpc_client.keep_alive.permit_without_stream- Send pings without streams (default: true)
Handler integration patterns:
The template demonstrates fetching from external service only. Alternative patterns are documented in handler code:
// Pattern 1: External only (current implementation)
user, err := h.grpcClient.GetUserByEmail(ctx, email)
// Pattern 2: Local database only (commented alternative)
// user, err := h.service.GetUserByEmail(ctx, email)
// Pattern 3: External with local fallback (commented alternative)
// user, err := h.grpcClient.GetUserByEmail(ctx, email)
// if err != nil {
// user, err = h.service.GetUserByEmail(ctx, email)
// }
// Pattern 4: Aggregate from both sources (commented alternative)
// externalUser, _ := h.grpcClient.GetUserByEmail(ctx, email)
// localUser, _ := h.service.GetUserByEmail(ctx, email)
// user = mergeUsers(externalUser, localUser)Test endpoints:
# Requires external user-service running on configured address
export HTTP_ENABLED=true
export HTTP_MOCK_AUTH=true
export GRPC_CLIENT_ENABLED=true
export GRPC_CLIENT_ADDRESS="user-service:9090"
make run
# Test the endpoint
curl -H "Authorization: Bearer test-token" \
"http://localhost:8080/users?email=test@example.com"Error handling:
- Returns 503 Service Unavailable when grpcClient is not configured
- Maps gRPC errors:
not found→ 404,invalid input→ 400,unavailable→ 503
For detailed gRPC development guide including adding new services and proto definitions, see docs/GRPC_GUIDE.md.
The WebSocket module provides real-time bidirectional communication with pub/sub support and room management using gorilla/websocket.
Enable WebSocket module:
export WEBSOCKET_ENABLED=true
export WEBSOCKET_HOST=0.0.0.0
export WEBSOCKET_PORT=8081
# Run the service
make runConfiguration options:
websocket.enabled- Enable/disable WebSocket server (default: false)websocket.host- Server host (default: 0.0.0.0)websocket.port- Server port (default: 8081)websocket.max_message_size- Max message size in bytes (default: 512000)websocket.limits.max_connections- Global connection limit (default: 0 = unlimited)websocket.limits.max_connections_per_room- Per-room limit (default: 0 = unlimited)
Connect and test with wscat:
# Install wscat
npm install -g wscat
# Connect
wscat -c ws://localhost:8081/ws
# Subscribe to a room
> {"type":"subscribe","room":"notifications"}
< {"type":"subscribed","room":"notifications","data":{"room":"notifications","client_count":1}}
# Publish to a room
> {"type":"publish","room":"notifications","data":{"message":"hello"}}
# Broadcast to all clients
> {"type":"broadcast","data":{"announcement":"server restart"}}Message types:
subscribe- Join a roomunsubscribe- Leave a roompublish- Send message to a room (must be subscribed)broadcast- Send message to all connected clientsping- Keepalive ping (server responds withpong)
Health endpoint:
curl http://localhost:8081/health
# Returns: {"status":"healthy","clients":5,"rooms":2}For detailed WebSocket development guide, see docs/WEBSOCKET_GUIDE.md.
See Module Development Guide for creating custom modules. The module system provides:
- Standard lifecycle: Init → Start → Stop with health checks
- Dependency injection: Modules depend on each other via constructor injection (explicit; no service locator)
- Configuration-driven: Enable/disable modules via YAML/env vars (repository depends on
database.enabled; service always registers) - Graceful shutdown: Automatic cleanup in reverse registration order
- Location:
internal/modelswith go-pg struct tags/hooks for database integration. - Validation: implement
Validate() errorand return*models.ValidationError(Field,Message) for structured errors. - Enums: typed ints with
String()and case-insensitiveUserStatusFromString(); add proto/JWT conversions later if needed. - Hooks:
BeforeInsert/BeforeUpdateconvert enums to strings and ensure UUID/timestamps;AfterSelectconverts strings back to enums.
// internal/models/user.go
user := &models.User{
Email: "test@example.com",
Name: "Jane Doe",
Status: models.UserActive,
}
if err := user.Validate(); err != nil {
if verr, ok := err.(*models.ValidationError); ok {
// structured error with field context
log.Printf("field=%s msg=%s", verr.Field, verr.Message)
}
return err
}// internal/models/widget.go
package models
type Widget struct {
ID uuid.UUID
Name string
State WidgetState
}
func (w *Widget) Validate() error {
if w.Name == "" {
return newValidationError("name", "is required")
}
if w.State < WidgetActive || w.State >= widgetStateUnsupported {
return newValidationError("state", "invalid value")
}
return nil
}// internal/models/user_status.go
status := models.UserActive
fmt.Println(status.String()) // "active"
parsed, err := models.UserStatusFromString("DELETED")
if err != nil {
// invalid value
}
fmt.Println(parsed == models.UserDeleted) // true// internal/models/widget_state.go
package models
type WidgetState int
const (
WidgetActive WidgetState = iota
WidgetDisabled
widgetStateUnsupported
)
var widgetStates = [...]string{
WidgetActive: "active",
WidgetDisabled: "disabled",
}
func (s WidgetState) String() string {
if s < 0 || int(s) >= len(widgetStates) {
return ""
}
return widgetStates[s]
}
func WidgetStateFromString(v string) (WidgetState, error) {
for i, r := range widgetStates {
if strings.EqualFold(v, r) {
return WidgetState(i), nil
}
}
return widgetStateUnsupported, fmt.Errorf("invalid widget state %q", v)
}This is a basic, generic Go microservice template designed to provide a clear structure and foundational tooling. It remains intentionally minimal.
- Defaults:
envdefaults toprod(config/init.go:setDefaults). - Precedence: flags > env vars > config file.
- Env var naming: dots become underscores (Viper replacer).
- To add a config field:
Precedence will ensure flag > env > config file for
// config/scheme.go type Scheme struct { Env string // existing Port int // new } // config/init.go func setDefaults() { viper.SetDefault("env", "prod") viper.SetDefault("port", 8080) } // cmd/root (bind a flag) cmd.Flags().Int("port", 0, "port to listen on")
portas well.
- Root command name:
microservice-template. - Subcommands:
serve(current runtime hook). Add more viacmd/<name>and register on root. - Version output:
./microservice-template --version(ldflags populatepkg/version). servelifecycle:PreRunlogs version;RunEshould start your workloads;PostRunalways stops app.- Adding a new command (example):
Register it in
// cmd/health/health.go package health import "github.com/spf13/cobra" func Cmd() *cobra.Command { return &cobra.Command{ Use: "health", Short: "Health probe", RunE: func(_ *cobra.Command, _ []string) error { // add checks here return nil }, } }
cmd/microservice-template.go:rootCmd.AddCommand(health.Cmd()).
- Format:
gofmt(used via go tooling). - Lint:
make lint(golangci-lint; see.golangci.yml). - Tests:
make testorgo test ./...; single test examplego test ./cmd/root -run TestInitializeConfig -count=1. - Build:
make build(CGO disabled; ldflags inject version info). - Deps:
make tidyafter changes;make updateto bump modules.
- Workflows:
.github/workflows/ci.ymland.github/workflows/release.yml.- CI (
ci.yml): on PRs andmain, runsmake lint,make test,make build(Go 1.24) with module caching. - Release (
release.yml): onmain, reruns lint/test/build, determines next incremental tag (v1,v2, …), pushes the tag, and creates a GitHub release with autogenerated notes (source-only). UsesGITHUB_TOKEN; no extra secrets needed.
- CI (
- Branch protection (recommended): require CI checks (
lint,test,build) to pass before merging tomainand limit direct pushes.
Makefileinjects name/tag/commit/branch/remote/build date intopkg/versionvia ldflags.pkg/versionformats a multi-line version string and handles unspecified values.- Sample output:
Template-service v0.0.0 Branch main, commit hash: abcdef123 Origin repository: https://github.com/org/repo Compiled at: 2026-01-16 20:58:09 +0000 UTC ©2026
pkg/logger.Log()returns a logrus logger with full timestamps.- Example:
log := logger.Log() log.Infof("starting service", "env=%s", cfg.Env) log.Errorf("failed to start: %v", err)
Note: To rename an existing project, see the Renaming the project section in Quickstart.
- Add config: update
Scheme,setDefaults, and CLI flags; test binding like incmd/root/root_test.go. - Add commands: create
cmd/<name>withcobra.Command, register on root incmd/microservice-template.go. After renaming the entrypoint file (e.g.,cmd/yourservice.go), register new commands there. - Add runtime logic: implement
App.Init/Serve/Stopwith proper context/shutdown handling and graceful shutdown. - Add tests: follow table-driven patterns; reset global state (Viper) in
t.Cleanup.
This project can receive updates from the upstream template: go-microservice-template.
make template-setupThis will:
- Add the template remote (
template) - Fetch the latest template changes
- Create
.template-versionto track sync state
make template-status
make template-diff # summary diff vs template/main
make template-diff v1.2.0 # diff against a specific tagmake template-fetch # fetch latest template changes
make template-sync # merge template/main into current branch
make template-sync v1.2.0 # merge a specific tagAfter merging:
- Resolve any conflicts manually
- Run tests:
make test(andmake buildif desired) - Commit with a clear message (e.g.,
chore: sync from template v1.2.0)
README.md,AGENTS.md(project-specific docs)internal/application.go(module registration)config/scheme.goandconfig/init.go(config schema/defaults)Makefile(custom targets)
- Sync regularly to reduce conflicts
- Keep sync commits separate from feature work
- Review
make template-diffbefore merging - Use
.template-versionto record the last synced template ref (updated automatically on successful sync)
Contributions are welcome! Please feel free to submit a Pull Request.
For development guidelines and best practices, see AGENTS.md.
This project is licensed under the MIT License — see the LICENSE file for details.
Copyright (c) 2022 Andrey Skurlatov