Decouple registration¶
NewServeMux
above declares an explicit dependency on EchoHandler
.
This is an unnecessarily tight coupling.
Does the ServeMux
really need to know the exact handler implementation?
If we want to write tests for ServeMux
,
we shouldn't have to construct an EchoHandler
.
Let's try to fix this.
-
Define a
Route
type in your main.go. This is an extension ofhttp.Handler
where the handler knows its registration path.// Route is an http.Handler that knows the mux pattern // under which it will be registered. type Route interface { http.Handler // Pattern reports the path at which this is registered. Pattern() string }
-
Modify
EchoHandler
to implement this interface.func (*EchoHandler) Pattern() string { return "/echo" }
-
In
main()
, annotate theNewEchoHandler
entry to state that the handler should be provided as a Route.fx.Provide( NewHTTPServer, NewServeMux, fx.Annotate( NewEchoHandler, fx.As(new(Route)), ), zap.NewExample, ),
-
Modify
NewServeMux
to accept a Route and use its provided pattern.// NewServeMux builds a ServeMux that will route requests // to the given Route. func NewServeMux(route Route) *http.ServeMux { mux := http.NewServeMux() mux.Handle(route.Pattern(), route) return mux }
-
Run the service.
{"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} {"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"} {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.As([[main.Route]])","type":"main.Route"} {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} {"level":"info","msg":"invoking","function":"main.main.func2()"} {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} {"level":"info","msg":"Starting HTTP server","addr":":8080"} {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"10.125µs"} {"level":"info","msg":"started"}
-
Send a request to it.
$ curl -X POST -d 'hello' http://localhost:8080/echo hello
What did we just do?
We introduced an interface to decouple the implementation
from the consumer.
We then annotated a previously provided constructor
with fx.Annotate
and fx.As
to cast its result to that interface.
This way, NewEchoHandler
was able to continue returning an *EchoHandler
.