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.

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 asFileServerNotFoundHandler and RedirectHandler. Let's begin with a simple but contrived example:

$ mkdir handler-example
$ cd handler-example
$ touch app.go
File: 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:

File: app.go
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.

File: app.go
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:

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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值