hello

The hello.example microservice is the main showcase example for the framework. Where helloworld shows the bare skeleton, hello collects many of the patterns a real microservice uses into a single package:

  • typed configs with defaults and validation
  • a ticker that fires on a fixed interval
  • a service-to-service call to calculator via its generated client
  • multicast discovery (Ping) over the management port
  • embedded static resources (image + YAML) served from the binary
  • locale-aware string lookup driven by the Accept-Language header
  • a root-mapped endpoint via the magic root hostname

It’s intentionally a grab-bag — the goal is breadth of demonstration, not modeling a production service.

Depends On

  • calculator.example — the Calculator UI endpoint delegates the actual computation to it via the generated calculatorapi client.

Try It

With the examples app running:

  • /hello?name=Bella — greeting driven by Greeting and Repeat configs:

    http://localhost:8080/hello.example/hello?name=Bella

    Ciao, Bella!
    Ciao, Bella!
    Ciao, Bella!
  • /echo — dumps the incoming HTTP request, including the framework’s Microbus-* control headers:

    http://localhost:8080/hello.example/echo

    GET /echo HTTP/1.1
    Host: hello.example
    Microbus-Call-Depth: 1
    Microbus-From-Host: http.ingress.core
    Microbus-From-Id: tg190vjj3j
    Microbus-Msg-Id: UQnfaJf4
    Microbus-Time-Budget: 19.749s
    ...
  • /ping — multicasts to every microservice on the bus, lists the responders:

    http://localhost:8080/hello.example/ping

    bvtgii68r8.messaging.example
    mv2pcoockl.messaging.example
    r52l78kha4.messaging.example
    pa6r5ohm5h.hello.example
    0iij3m5fhf.http.ingress.core
    7k9f82n45f.configurator.core
    n89hmtb9iq.calculator.example
  • /calculator — HTML form that posts back through the calculator service:

    http://localhost:8080/hello.example/calculator

  • /bus.png — static image served from the embedded resources/ directory.

  • /localized — picks a translation from resources/text.yaml based on the Accept-Language header.

    http://localhost:8080/hello.example/localized

    en: Hello       fr: Bonjour    es: Hola      it: Salve
    de: Guten Tag   pt: Olá        da: Goddag    nl: Goedendag
    pl: Dzień dobry no: God dag    tr: Merhaba   sv: God dag
  • / — root page, served from the magic root hostname.

    http://localhost:8080/

Code Walkthrough

Source: exampleservices/hello/service.go.

The Service struct holds no state of its own; everything it needs comes from the embedded *Intermediate:

type Service struct {
    *Intermediate // IMPORTANT: Do not remove
}

OnStartup and OnShutdown are empty.

Wiring (intermediate.go)

The constructor plus the salient subscriptions, ticker, and configs from the generated intermediate.go:

func NewService() *Service {
    svc := &Service{}
    svc.Intermediate = NewIntermediate(svc)
    return svc
}

Each web endpoint is a sub.Web() subscription — the handler signature is (w http.ResponseWriter, r *http.Request):

svc.Subscribe("Hello",        svc.Hello,        sub.At(...), sub.Web())
svc.Subscribe("Echo",         svc.Echo,         sub.At(...), sub.Web())
svc.Subscribe("Ping",         svc.Ping,         sub.At(...), sub.Web())
svc.Subscribe("Calculator",   svc.Calculator,   sub.At(...), sub.Web())
svc.Subscribe("BusPNG",       svc.BusPNG,       sub.At(...), sub.Web())
svc.Subscribe("Localization", svc.Localization, sub.At(...), sub.Web())
svc.Subscribe("Root",         svc.Root,         sub.At(...), sub.Web())

The ticker fires TickTock every 10 seconds:

svc.StartTicker("TickTock", 10*time.Second, svc.TickTock)

Configs are declared with defaults and (optionally) validation rules — and produce typed accessor methods on *Service:

svc.DefineConfig(
    "Greeting",
    cfg.Description(`Greeting to use.`),
    cfg.DefaultValue("Hello"),
)
svc.DefineConfig(
    "Repeat",
    cfg.Description(`Repeat indicates how many times to display the greeting.`),
    cfg.DefaultValue("1"),
    cfg.Validation("int [0,100]"),
)

After this, svc.Greeting() returns a string and svc.Repeat() returns an int — the conversion from the raw string config value happens in the generated code, so handlers never deal with svc.Config("Repeat") + manual parsing.

Hello

func (svc *Service) Hello(w http.ResponseWriter, r *http.Request) error {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "World"
    }

    greeting := svc.Greeting()
    hello := greeting + ", " + name + "!\n"
    hello = strings.Repeat(hello, svc.Repeat())

    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte(hello))
    return nil
}

The simplest config consumer in the example. svc.Greeting() and svc.Repeat() are the typed accessors generated from the DefineConfig calls above. Set them in main/config.yaml to change the greeting without rebuilding.

Echo

func (svc *Service) Echo(w http.ResponseWriter, r *http.Request) error {
    // Prevent the http package from serializing Go-http-client/1.1 as the user-agent
    if len(r.Header.Values("User-Agent")) == 0 {
        r.Header.Set("User-Agent", "")
    }
    w.Header().Set("Content-Type", "text/plain")
    err := r.Write(w)
    return errors.Trace(err)
}

The interesting output isn’t the body the user sent — it’s the headers the framework added on the way in. Microbus-Call-Depth, Microbus-From-Host, Microbus-From-Id, Microbus-Msg-Id, Microbus-Time-Budget are the framework’s transport metadata, propagated on every call. Echo is a handy diagnostic when wiring up new services or chasing a tracing/identity issue.

Ping

func (svc *Service) Ping(w http.ResponseWriter, r *http.Request) error {
    var buf bytes.Buffer
    ch := svc.Publish(
        r.Context(),
        pub.GET("https://all:888/ping"),
        pub.Multicast(),
    )
    for i := range ch {
        res, err := i.Get()
        if err == nil {
            fromHost := frame.Of(res).FromHost()
            fromID := frame.Of(res).FromID()
            buf.WriteString(fromID)
            buf.WriteString(".")
            buf.WriteString(fromHost)
            buf.WriteString("\r\n")
        }
    }

    w.Header().Set("Content-Type", "text/plain")
    w.Write(buf.Bytes())
    return nil
}

Ping demonstrates multicast over the management port :888:

  • The hostname all is the magic “broadcast to every service on the bus” target.
  • Port :888 is the framework’s management port — it serves built-in control endpoints like /ping and /openapi.json on every microservice. It’s reachable only over the bus (not through the HTTP ingress proxy), so calls to it are inherently internal.
  • pub.Multicast() says “expect many responses, not one.” The returned channel yields each response as it arrives; frame.Of(res).FromHost() and .FromID() extract the responder’s identity from the response headers.

Calculator

func (svc *Service) Calculator(w http.ResponseWriter, r *http.Request) error {
    // ... HTML form scaffolding for X, op, Y inputs ...

    if x != "" && y != "" && op != "" {
        xx, err := strconv.Atoi(x)
        if err != nil {
            return errors.Trace(err)
        }
        yy, err := strconv.Atoi(y)
        if err != nil {
            return errors.Trace(err)
        }
        // Call the calculator service using its client
        _, _, _, result, err := calculatorapi.NewClient(svc).Arithmetic(r.Context(), xx, op, yy)
        if err != nil {
            buf.WriteString(html.EscapeString(err.Error()))
        } else {
            buf.WriteString(strconv.Itoa(result))
        }
    }
    // ... form submit, response ...
}

The HTML scaffolding is omitted above for brevity; the salient point is the service-to-service call in the middle: calculatorapi.NewClient(svc).Arithmetic(r.Context(), xx, op, yy). The generated client makes the call over the bus and unmarshals the response into typed return values — there’s no URL construction, no JSON parsing, no pub.Request here. Errors from the calculator are written into the rendered HTML rather than bubbled up, because this is a UI handler.

BusPNG

func (svc *Service) BusPNG(w http.ResponseWriter, r *http.Request) (err error) {
    return svc.ServeResFile("bus.png", w, r)
}

ServeResFile serves a file from the package’s embedded resources/ filesystem (the FS is registered in intermediate.go via svc.SetResFS(resources.FS)). The content type is inferred from the extension.

Localization

func (svc *Service) Localization(w http.ResponseWriter, r *http.Request) (err error) {
    ctx := r.Context()
    hello, _ := svc.LoadResString(ctx, "Hello")
    w.Write([]byte(hello))
    return nil
}

LoadResString looks up the key "Hello" in resources/text.yaml and returns the best translation for the request’s Accept-Language header. The matching logic — including weighting and fallback to a default language — is handled by the framework; the handler just asks for a string by key.

Root

func (svc *Service) Root(w http.ResponseWriter, r *http.Request) (err error) {
    var buf bytes.Buffer
    buf.WriteString(`<html><body><h1>Microbus</h1></body></html>`)
    w.Write(buf.Bytes())
    return nil
}

Root is mapped to the route //root (absolute path beginning with //). The leading double slash routes the handler under the magic root hostname, which the HTTP ingress proxy exposes at the server root (/) rather than under /hello.example/.... This is the pattern for a landing page or any other endpoint that should live at the ingress root.

TickTock

func (svc *Service) TickTock(ctx context.Context) error {
    svc.LogInfo(ctx, "Ticktock")
    return nil
}

The handler signature for a ticker is just (ctx context.Context) error — no HTTP because it’s not invoked as a request. The schedule (10*time.Second) is set in the StartTicker call shown in Wiring. Tickers are the standard place to put periodic housekeeping: cache eviction, batch flushing, health probes against external systems.

See Also