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¶
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¶
- Add auth, security headers & a gateway — compose authentication, security headers and a REST gateway onto these servers.
- Stream a response — and survive the write deadline the server just applied to your handler.
- HTTP server reference — every option, its default, and what happens when it is wrong.
- What transport does not do — the things this module deliberately leaves to someone else.
- The server/client split & the lifecycle seam — why the servers live here and the framework config adapters stay in go-tool-base.