A Recap of Request Handling in Go
http://www.alexedwards.net/blog/a-recap-of-request-handling
Processing HTTP requests with Go is primarily about two things: ServeMuxes and Handlers.
A ServeMux is essentially a HTTP request router (or multiplexor). It compares incoming requests against a list of predefined URL paths, and calls the associated handler for the path whenever a match is found.
Handlers are responsible for writing response headers and bodies. Almost any object can be a handler, so long as it satisfies the Handler
interface. In lay terms, that simply means it must have a ServeHTTP
method with the following signature:
ServeHTTP(ResponseWriter, *Request)
Go's HTTP package ships with a few functions to generate common handlers, such asFileServer, NotFoundHandler and RedirectHandler. Let's begin with a simple but contrived example:
$ mkdir handler-example $ cd handler-example $ touch app.goFile: app.go
package main import ( "log" "net/http" ) func main() { mux := http.NewServeMux() h := http.RedirectHandler("http://example.org", 307) mux.Handle("/foo", h) log.Println("Listening...") http.ListenAndServe(":3000", mux) }
Let's step through this quickly:
- In the
main
function we use the NewServeMux function to create an empty ServeMux. - We then use the RedirectHandler function to
create a new handler, which temporarily redirects all requests it receives to
http://example.org
. - Next we use the Handle function to register
this with our new ServeMux, so it acts as the handler for all incoming requests with the URL path
/foo
. - Finally we create a new server and start listening for incoming requests with theListenAndServe function, passing in our ServeMux for it to match requests against.
Go ahead and run the application:
$ go run app Listening...
And visit localhost:3000/foo in your browser. You should find your request gets successfully redirected.
The eagle-eyed of you might have noticed something interesting: The signature for the ListenAndServe function is ListenAndServe(addr string, handler Handler)
, but
we passed a ServeMux as the second parameter.
We were able to do this because ServeMux has a ServeHTTP method, meaning that it satisfies the Handler interface.
For me it simplifies things to think of a ServeMux as just being a special kind of handler, which instead of providing a response itself passes the request on to a second handler. This isn't as much of a leap as it first sounds – chaining of handlers is fairly commonplace in Go, and we'll take a look at an example further down the page.
Custom Handlers
Let's create a custom handler which responds with the current time in a given time zone, formatted in RFC 1123 format:
type timeHandler struct { zone *time.Location } func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { tm := time.Now().In(th.zone).Format(time.RFC1123) w.Write([]byte("The time is: " + tm)) }
The exact code here isn't too important.
All that really matters is that we have an object (in this case it's a timeHandler
struct), and we've implemented a ServeHTTP
method
on it which returns a HTTP response. That's all we need to make a handler.
Let's embed the timeHandler
in a concrete example:
package main import ( "log" "net/http" "time" ) type timeHandler struct { zone *time.Location } func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { tm := time.Now().In(th.zone).Format(time.RFC1123) w.Write([]byte("The time is: " + tm)) } func newTimeHandler(name string) *timeHandler { return &timeHandler{zone: time.FixedZone(name, 0)} } func main() { mux := http.NewServeMux() mux.Handle("/est", newTimeHandler("EST")) log.Println("Listening...") http.ListenAndServe(":3000", mux) }
Run the application and visit localhost:3000/est in your browser. You should get the current Eastern Standard Time as a response.
Functions as Handlers
Alternatively, Go's HTTP package provides a convenient way to turn normal functions into handlers, thanks to the HandlerFunc type.
You can convert any function with the signature func(ResponseWriter, *Request)
into a HandlerFunc object. Every HandlerFunc has a ServeHTTP
method
built in, which simply calls the original function. If that sounds confusing, try taking a look at the Go
source code.
It's a roundabout but very succinct way of turning a function into something that satisfies the Handler interface.
Let's reproduce the time zone application using this approach:
File: app.go
package main import ( "log" "net/http" "time" ) func main() { mux := http.NewServeMux() mux.Handle("/est", http.HandlerFunc(est)) log.Println("Listening...") http.ListenAndServe(":3000", mux) } func est(w http.ResponseWriter, r *http.Request) { tm := time.Now().In(time.FixedZone("EST", 0)).Format(time.RFC1123) w.Write([]byte("The time is: " + tm)) }
In fact, converting a function to a HandlerFunc and adding it to a ServeMux like this is so common that Go provides a shortcut: the HandleFunc method.
We'll swap our example to use the shortcut instead:
File: app.go
... func main() { mux := http.NewServeMux() // mux.Handle("/est", http.HandlerFunc(est)) mux.HandleFunc("/est", est) log.Println("Listening...") http.ListenAndServe(":3000", mux) } ...
The DefaultServeMux
You've probably seen the DefaultServeMux mentioned in lots of places, from the simplest Hello World examples to the Go source code.
It took me a long time to realise there is nothing special about this. The DefaultServeMux is just a plain ol' ServeMux like we've already been using, which gets instantiated by default when the HTTP package is used. Here's the relevant line from the Go source:
var DefaultServeMux = NewServeMux()
The HTTP package provides a couple of shortcuts for working with the DefaultServeMux:http.Handle and http.HandleFunc. These do exactly the same as their namesake functions we've already looked at, with the difference that they add handlers to the DefaultServeMux instead of a manually created one.
Additionally, ListenAndServe will fall back to using the DefaultServeMux if no other handler is provided.
Changing our time zone application to use the DefaultServeMux instead is easy:
File: app.go
... func main() { http.HandleFunc("/est", est) log.Println("Listening...") http.ListenAndServe(":3000", nil) } ...
Chaining Handlers
Joining handlers together can be a nice way to share common functionality or create 'layers' in your application. Let's adapt our time zone application to include an intermediarylogHandler
which
records the method and path of requests.
package main import ( "log" "net/http" "time" ) type logHandler struct { next http.Handler } func (lh *logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Printf("%v: %v", r.Method, r.URL.Path) lh.next.ServeHTTP(w, r) } func newLogHandler(next http.Handler) *logHandler { return &logHandler{next} } func main() { http.Handle("/est", newLogHandler(http.HandlerFunc(est))) log.Println("Listening...") http.ListenAndServe(":3000", nil) } func est(w http.ResponseWriter, r *http.Request) { tm := time.Now().In(time.FixedZone("EST", 0)).Format(time.RFC1123) w.Write([]byte("The time is: " + tm)) }
You can probably see what's going on easily enough. When creating the logHandler
we pass the final handler as a parameter and store it in the logHandler
struct.
After the logger is done recording the request, we simply call the next handler in line with the codelh.next.ServeHTTP(w, r)
.
So far, so good. We can also set up our intermediary handler as a function:
File: app.go
package main import ( "log" "net/http" "time" ) func logHandler(next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log.Printf("%v: %v", r.Method, r.URL.Path) next.ServeHTTP(w, r) } } func main() { http.Handle("/est", logHandler(http.HandlerFunc(est))) log.Println("Listening...") http.ListenAndServe(":3000", nil) } func est(w http.ResponseWriter, r *http.Request) { tm := time.Now().In(time.FixedZone("EST", 0)).Format(time.RFC1123) w.Write([]byte("The time is: " + tm)) }
This is pretty neat. Our logHandler
function simply returns a closure containing the logic as a HandlerFunc type (remember – converting to a HandlerFunc essentially
implements aServeHTTP
method on the closure, so it can be used like any other handler).
You can see this approach to chaining handlers in some third-party packages like Ghost andHandy.
Lastly let's complete things by adding an adapter, so we can chain normal functions to ourlogHandler
without manually converting them first:
package main import ( "log" "net/http" "time" ) func logHandler(next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log.Printf("%v: %v", r.Method, r.URL.Path) next.ServeHTTP(w, r) } } func logHandlerFunc(next http.HandlerFunc) http.HandlerFunc { return LogHandler(next) } func main() { http.Handle("/est", logHandlerFunc(est)) log.Println("Listening...") http.ListenAndServe(":3000", nil) } func est(w http.ResponseWriter, r *http.Request) { tm := time.Now().In(time.FixedZone("EST", 0)).Format(time.RFC1123) w.Write([]byte("The time is: " + tm)) }
If you found this post useful, you might like to subscribe to my RSS feed.
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。