Skip to content

Getting started

This tutorial stands up a hardened HTTP server with health endpoints, then adds a gRPC server alongside it. That is enough to see what the stack wires for you.

Install

go get gitlab.com/phpboyscout/go/transport

A health-checked HTTP server

transport/http builds a hardened *http.Server from typed ServerSettings, and gives you ready-made health/liveness/readiness handlers that report from a go/controls health source:

package main

import (
    "context"
    "net/http"

    transporthttp "gitlab.com/phpboyscout/go/transport/http"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/healthz", transporthttp.HealthHandler(reporter))    // controls.HealthReporter
    mux.HandleFunc("/livez", transporthttp.LivenessHandler(reporter))
    mux.HandleFunc("/readyz", transporthttp.ReadinessHandler(reporter))

    srv, err := transporthttp.NewServer(
        context.Background(),
        transporthttp.ServerSettings{Port: 8080},
        mux,
    )
    if err != nil {
        panic(err)
    }

    _ = srv // hand to go/controls, or srv.ListenAndServe()
}

NewServer sets bounded read/write/idle timeouts and a hardened TLS config; health endpoints are plain handlers you mount outside any auth middleware, so probes are never gated by authentication.

Restricting the bind address

By default a server binds all interfaces (:port). Set Host on ServerSettings, or pass WithHost / WithBindAddress, to restrict the listener to a single interface. Loopback-only, for example, makes the port unreachable off-host:

srv, err := transporthttp.NewServer(
    ctx,
    transporthttp.ServerSettings{Host: "127.0.0.1", Port: 8080}, // loopback only
    mux,
    // transporthttp.WithHost("127.0.0.1"), // equivalently, via option
)

An empty Host binds all interfaces. The gRPC transport carries the same Host field and WithHost / WithBindAddress options (the DialLocal client path is unaffected, because it always targets localhost).

Option safety

Four entry points take an opts ...any variadic rather than a typed one: http.Register, grpc.NewServer, grpc.Register and grpc.DialLocal. Each accepts a fixed set of option families (ServerOption, RegisterOption, and the underlying grpc.ServerOption / grpc.DialOption) and rejects anything else with an error naming the type it refused, rather than silently discarding it. Hand a dial option to a server constructor and you find out immediately.

http.NewServer is typed (opts ...ServerOption), so the same mistake there is a compile error instead. The per-function list of what each one accepts is in the HTTP and gRPC reference.

Run it under the lifecycle

The server is designed to be driven by go/controls. Register wires construction, health, and graceful shutdown into a supervised service in one call:

import (
    gtls "gitlab.com/phpboyscout/go/tls"
    transporthttp "gitlab.com/phpboyscout/go/transport/http"
)

srv, err := transporthttp.Register(
    ctx, "api", controller, logger,      // controls.Controllable + *slog.Logger
    handler,
    transporthttp.ServerSettings{Port: 8080},
    transporthttp.WithMiddleware(chain),  // a go/transit server Chain (optional)
    // No TLS option ⇒ plain HTTP. To serve TLS, add one of:
    //   transporthttp.WithTLSPair(gtls.Pair{Enabled: true, Cert: …, Key: …})
    //   transporthttp.WithServedCertificate(cert)   // an in-memory *tls.Certificate
)

Add a gRPC server

transport/grpc mirrors the shape for gRPC: a secure *grpc.Server, a built-in health service, and the same lifecycle glue:

import transportgrpc "gitlab.com/phpboyscout/go/transport/grpc"

srv, err := transportgrpc.NewServer(transportgrpc.ServerSettings{Port: 9090, Reflection: true})
if err != nil {
    panic(err)
}

// RegisterHealthService starts a background goroutine that refreshes the health
// status every ten seconds. Wiring a server by hand means you own that
// goroutine: call the returned stop function when the server is done.
//
// It needs the controller itself, not a controls.HealthReporter: it reads the
// controller's context to bound the goroutine, which HealthReporter does not
// expose.
stopHealth := transportgrpc.RegisterHealthService(srv, controller)
defer stopHealth()

// pb.RegisterYourServiceServer(srv, impl)

start := transportgrpc.Start(logger, srv, transportgrpc.ServerSettings{Port: 9090}, gtls.Pair{})
_ = start // a controls.StartFunc

Register binds that goroutine to the service lifecycle for you (it runs only while the server is serving), so on that path there is nothing to stop by hand.

Leave Reflection off unless you want it. It is set here to show the field, and it makes the server enumerable: the auth interceptor exempts reflection unconditionally, so a caller with no credential can list every service and method you expose.

Where next