How to Register and Manage Services Using serv.RegisterService in Gorig

Use serv.RegisterService to register one or more Service structs during application bootstrap, then invoke serv.Running() to start all registered services and handle graceful shutdown.

The serv package in the jom-io/gorig repository provides a centralized service lifecycle manager for Go applications. By using serv.RegisterService, you can decouple service definition from execution, allowing HTTP servers, background workers, and custom processes to register themselves during initialization and start uniformly under a single control loop.

Understanding the Service Architecture in Gorig

At the core of Gorig's service management is the Service struct defined in serv/serv.go:

type Service struct {
    Code     string                               // unique identifier
    PORT     string                               // optional listening port
    Startup  func(code, port string) error        // called when the service starts
    Shutdown func(code string, ctx context.Context) error // called on graceful stop
}

All registered services are stored in a private global map gServices (map[string]Service). The RegisterService function safely inserts one or more Service instances into this map, rejecting duplicates by checking against existing codes via the internal doRegisterService helper.

How to Register a Service with serv.RegisterService

The RegisterService function accepts variadic Service structs and returns an error if registration fails:

func RegisterService(service ...Service) *errors.Error

Registration should occur during the application bootstrap phase, typically in bootstrap/startup.go or an equivalent initialization file. Each service must have a unique Code to prevent collisions in the global registry.

Registering the Built-in HTTP Service

The following example from bootstrap/startup.go demonstrates how Gorig registers its default HTTP server using httpx package implementations:

func regWebService() {
    err := serv.RegisterService(serv.Service{
        Code:     "HTTP",
        PORT:     configure.GetString("api.rest.addr", ":9617"),
        Startup:  httpx.Startup,   // func(string,string) error
        Shutdown: httpx.Shutdown,  // func(string,context.Context) error
    })
    if err != nil {
        sys.Exit(err) // abort if registration fails
    }
}

This registration pulls the port from configuration and delegates the actual server logic to the httpx package, maintaining clean separation between the service registry and implementation.

Registering a Custom Background Worker

For background processes that do not bind to a port, omit the PORT field and implement custom Startup and Shutdown logic:

package workers

import (
    "context"
    "time"
    
    "github.com/jom-io/gorig/serv"
    "github.com/jom-io/gorig/utils/sys"
)

func runWorker(ctx context.Context) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            sys.Info("processing background task")
        case <-ctx.Done():
            sys.Info("worker received shutdown signal")
            return
        }
    }
}

func startup(code, _ string) error {
    go runWorker(context.Background())
    sys.Success("worker service started")
    return nil
}

func shutdown(_ string, ctx context.Context) error {
    // Context cancellation handled in runWorker via ctx.Done()
    return nil
}

func RegisterWorker() {
    if err := serv.RegisterService(serv.Service{
        Code:     "WORKER",
        Startup:  startup,
        Shutdown: shutdown,
    }); err != nil {
        sys.Exit(err)
    }
}

This pattern allows long-running goroutines to participate in the unified lifecycle managed by the serv package.

Managing the Service Lifecycle

Once services are registered, the serv package provides three primary mechanisms for lifecycle control: bulk startup, selective startup, and graceful shutdown.

Starting All Services

The Running() function initiates the main application loop. It iterates over gServices, invokes each Startup function, and blocks until an interrupt signal is received:

// In bootstrap/startup.go or main.go
serv.Running()

Under the hood, Running() handles OS signals (SIGINT, SIGTERM) and coordinates the shutdown sequence when the process needs to terminate.

Selective Service Startup with StartCode

For scenarios requiring dynamic or conditional service startup, use StartCode(code string) to launch a specific service by its registered code:

// Start only the background worker
if err := serv.StartCode("WORKER"); err != nil {
    sys.Error("failed to start worker:", err)
}

This is useful for CLI tools that run specific services independently or for testing individual components without starting the entire application stack.

Graceful Shutdown Handling

When Running() receives an interrupt signal, it creates a timeout context (default 5 seconds) and invokes each service's Shutdown function in reverse registration order:

// Shutdown signature implemented by services
func Shutdown(code string, ctx context.Context) error

Services should respect the context cancellation to ensure timely termination. The HTTP service in httpx/http.go demonstrates this by calling server.Shutdown(ctx) within its shutdown handler.

Complete Bootstrap Example

The following initialization sequence demonstrates registering multiple service types and launching the application:

package bootstrap

import (
    "github.com/jom-io/gorig/serv"
    "github.com/jom-io/gorig/httpx"
    "github.com/jom-io/gorig/workers"
    "github.com/jom-io/gorig/utils/configure"
    "github.com/jom-io/gorig/utils/sys"
)

func StartUp() {
    // Register HTTP API service
    err := serv.RegisterService(serv.Service{
        Code:     "HTTP",
        PORT:     configure.GetString("api.rest.addr", ":9617"),
        Startup:  httpx.Startup,
        Shutdown: httpx.Shutdown,
    })
    if err != nil {
        sys.Exit(err)
    }

    // Register background worker
    workers.RegisterWorker()

    // Start all registered services and block until shutdown
    serv.Running()
}

This pattern scales to any number of services while maintaining clean separation between business logic and lifecycle management.

Summary

  • Service Definition: Create a serv.Service struct with unique Code, optional PORT, and Startup/Shutdown functions.
  • Registration: Call serv.RegisterService() during bootstrap to add services to the global gServices registry; duplicates are rejected automatically.
  • Bulk Startup: Invoke serv.Running() to start all registered services and block until an interrupt signal triggers graceful shutdown.
  • Selective Startup: Use serv.StartCode("CODE") to launch individual services dynamically without affecting others.
  • Graceful Shutdown: Implement Shutdown functions to respect context cancellation, ensuring services terminate cleanly within the timeout window managed by serv.Running().

Frequently Asked Questions

What happens if I try to register two services with the same Code?

The RegisterService function returns an error if a service with the specified Code already exists in the gServices map. According to the implementation in serv/serv.go, the internal doRegisterService helper checks for existing entries before insertion, preventing duplicate registrations and ensuring each service maintains a unique identifier.

Can I register a service without a Shutdown function?

While the Service struct allows nil functions, it is strongly recommended to provide a Shutdown implementation for any service that starts background goroutines or holds resources. The serv.Running() function invokes Shutdown for every registered service during graceful termination, passing a timeout context. If Shutdown is nil or unimplemented, the service may not terminate cleanly, potentially causing goroutine leaks or resource exhaustion.

How do I start only specific services for testing?

Use the serv.StartCode(code string) function to launch individual services by their registered identifier. This method bypasses the bulk startup logic in serv.Running() and invokes only the specified service's Startup function. For testing scenarios, you can register multiple services but selectively start only the component under test, leaving others dormant until explicitly needed.

Where should I place the RegisterService calls in my application?

Place RegisterService calls in your bootstrap or initialization package, typically in a file like bootstrap/startup.go. This ensures services are registered before serv.Running() is invoked in your main function. The registration phase should occur after configuration loading but before the application enters its runtime loop, ensuring all service codes are validated and the global registry is fully populated before startup begins.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →