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¶
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¶
- 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.