go/docs/services.md
Snider 89d189dd95 docs: add human-friendly documentation for Core Go framework
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:02:37 +00:00

6 KiB

title description
Services Service registration, retrieval, ServiceRuntime, and factory patterns.

Services

Services are the building blocks of a Core application. They are plain Go structs registered into a named registry and retrieved by name with optional type assertions.

Registration

Factory Functions

The primary way to register a service is via a factory function -- a function with the signature func(*Core) (any, error). The factory receives the Core instance so it can access other services or register message handlers during construction.

func NewMyService(c *core.Core) (any, error) {
    return &MyService{}, nil
}

WithService (auto-named)

WithService registers a service and automatically discovers its name from the Go package path. The last segment of the package path becomes the service name, lowercased.

// If MyService lives in package "myapp/services/calculator",
// it is registered as "calculator".
c, err := core.New(
    core.WithService(calculator.NewService),
)

WithService also performs IPC handler discovery: if the returned service has a method named HandleIPCEvents with the signature func(*Core, Message) error, it is automatically registered as an action handler.

type Service struct{}

func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
    // Handle messages
    return nil
}

WithName (explicitly named)

When you need to control the service name (or the factory is an anonymous function), use WithName:

c, err := core.New(
    core.WithName("my-service", func(c *core.Core) (any, error) {
        return &MyService{}, nil
    }),
)

Unlike WithService, WithName does not auto-discover IPC handlers. Register them manually if needed.

Direct Registration

You can also register a service directly on an existing Core instance:

err := c.RegisterService("my-service", &MyService{})

This is useful for tests or when constructing services outside the New() options flow.

Registration Rules

  • Service names must not be empty.
  • Duplicate names are rejected with an error.
  • If WithServiceLock() was passed to New(), registration after initialisation is rejected.

Retrieval

By Name (untyped)

svc := c.Service("calculator")
if svc == nil {
    // not found
}

Returns nil if no service is registered under that name.

Type-Safe Retrieval

ServiceFor[T] retrieves and type-asserts in one step:

calc, err := core.ServiceFor[*calculator.Service](c, "calculator")
if err != nil {
    // "service 'calculator' not found"
    // or "service 'calculator' is of type *Foo, but expected *calculator.Service"
}

Panicking Retrieval

For init-time wiring where a missing service is a fatal programming error:

calc := core.MustServiceFor[*calculator.Service](c, "calculator")
// panics if not found or wrong type

ServiceRuntime

ServiceRuntime[T] is a generic helper you embed in your service struct. It provides typed access to the Core instance and your service's options struct.

type Options struct {
    Precision int
}

type Service struct {
    *core.ServiceRuntime[Options]
}

func NewService(opts Options) func(*core.Core) (any, error) {
    return func(c *core.Core) (any, error) {
        return &Service{
            ServiceRuntime: core.NewServiceRuntime(c, opts),
        }, nil
    }
}

ServiceRuntime provides these methods:

Method Returns Description
Core() *Core The central Core instance
Opts() T The service's typed options
Config() Config Convenience shortcut for Core().Config()

Real-World Example: The Log Service

The pkg/log package in this repository is the reference implementation of a Core service:

type Service struct {
    *core.ServiceRuntime[Options]
    *Logger
}

func NewService(opts Options) func(*core.Core) (any, error) {
    return func(c *core.Core) (any, error) {
        logger := New(opts)
        return &Service{
            ServiceRuntime: core.NewServiceRuntime(c, opts),
            Logger:         logger,
        }, nil
    }
}

func (s *Service) OnStartup(ctx context.Context) error {
    s.Core().RegisterQuery(s.handleQuery)
    s.Core().RegisterTask(s.handleTask)
    return nil
}

Key patterns to note:

  1. The factory is a closure -- NewService takes options and returns a factory function.
  2. ServiceRuntime is embedded, giving access to Core() and Opts().
  3. The service implements Startable to register its query/task handlers at startup.

Runtime and NewWithFactories

For applications that wire services from a map of named factories, NewWithFactories offers a bulk registration path:

type ServiceFactory func() (any, error)

rt, err := core.NewWithFactories(app, map[string]core.ServiceFactory{
    "config":   configFactory,
    "database": dbFactory,
    "cache":    cacheFactory,
})

Factories are called in sorted key order. The resulting Runtime wraps a Core and exposes ServiceStartup/ServiceShutdown for GUI runtime integration.

For the simplest case with no custom services:

rt, err := core.NewRuntime(app)

Well-Known Services

Core provides convenience methods for commonly needed services. These use MustServiceFor internally and will panic if the service is not registered:

Method Expected Name Expected Interface
c.Config() "config" Config
c.Display() "display" Display
c.Workspace() "workspace" Workspace
c.Crypt() "crypt" Crypt

These are optional -- only call them if you have registered the corresponding service.

Thread Safety

The service registry is protected by sync.RWMutex. Registration, retrieval, and lifecycle operations are safe to call from multiple goroutines.