Skip to content

Getting started

This tutorial stands up a hardened HTTP server with health endpoints, then adds a gRPC server alongside it — 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 — for example loopback-only so the port is 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
)

Host is additive: an empty value preserves the previous all-interfaces behaviour. The gRPC transport carries the same Host field and WithHost / WithBindAddress options (the DialLocal client path is unaffected — 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