The http.HandlerFunc wrapper technique in #golang

Mat Ryer · · 2146 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

tl;dr: functions that take a http.HandlerFunc and return a new one can do things before and/or after the handler is called, and even decide whether to call the original handler at all.

If you’re building web services using Go (if you’re not, why not?) and you’re not using any middleware packages (and even if you are), then you need to understand the power of wrapping `http.HandlerFunc` functions.

An http.HandlerFunc wrapper is a function that has one input argument and one output argument, both of type http.HandlerFunc.

Wrappers have the following signature:

func(http.HandlerFunc) http.HandlerFunc

The idea is that you take in an `http.HandlerFunc` and return a new one that does something else before and/or after calling the original. For example, a simple logging wrapper might look like this:

func log(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("Before")
fn(w, r)
log.Println("After")
}
}

Here our `log` function returns a new function (Go will convert it into an `http.HandlerFunc` for us) that will print the “Before” string and call the original handler before printing out the “After” string.

Now, wherever I might pass my original `http.HandlerFunc` I can wrap it so that:

http.HandleFunc("/path", handleThing)

becomes

http.HandleFunc("/path", log(handleThing))

When would you use wrappers?

This approach can be used to address lots of different situations, including but not limited to:

  • Logging and tracing
  • Connecting and disconnecting to databases
  • Validating the request, such as checking authentication credentials
  • Writing common response headers

Sharing state

If our `http.HandlerFunc` were to create some useful object that our original handler might want to use (such as a database connection), we need to make that object available to the handlers. A common and forgivable solution is to create your own alternative to the `http.HandlerFunc` where you take the object as an additional argument. This is not recommended, because suddenly your code stops working with normal `http.HandlerFunc` functions. Instead, consdier a solution like Gorilla’s context package.

To learn more about how Gorilla’s context package works or to roll your own, see my book: Chapter 6, page 159 of Go Programming Blueprints. #shamelessplug

Essentially, it stores an in-memory map of objects keyed on the `http.Request`. Think of something like this:

map[*http.Request]map[string]interface{}

In Go speak, this is a `map` where a pointer to the request (`*http.Request`) is the key, and another map (`map[string]interface{}`) is the value. This allows us to store a map of objects for each request.

Our wrapper handler would store the object in the map for the request (remember they both have access to the `http.Request`) and the wrapped handlers can go and fetch it. The context package abstracts this for us and allows us to use helper functions:

// in our wrapper - use context.Set
context.Set(r, "nameOfThing", thing)
// inside our original handlers, use context.Get
obj := context.Get(r, "nameOfThing").(thingType)

To make sure these objects get cleaned up after each request, we need to call `context.Clear` or use the `context.ClearHandler`.

To call or not to call

Wrappers also get to make the decision about whether to call the original handler or not. If they want to, they can even respond on their own. Let’s say a `key` parameter is mandatory in our API:

func checkAPIKey(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if len(r.URL.Query().Get("key") == 0) {
http.Error(w, "missing key", http.StatusUnauthorized)
return // don't call original handler
}
fn(w, r)
}
}

The `checkAPIKey` wrapper will make sure there is a key, and if not will return with an Unauthorized error. It could be extended to validate the key in a datastore, and ensure rate limits haven’t been reached.

Deferring

Using Go’s `defer` statement, we can add code that we can be sure will run whatever happens inside our original `http.HandlerFunc`.

func log(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("Before")
defer log.Println("After")
fn(w, r)
}
}

Now, even if our code inside the original handler panics, we’ll still see the “After” line printed. Other examples of functions you might defer include closing a database connection or kicking off a background task.

Passing arguments to the wrappers

While we want to avoid changing the signature of our handlers, it is OK to take additional arguments into our wrapper functions. We might do this so that our wrappers can be reused with slight variants.

If we want to write a wrapper that will insist on certain parameters being present in the request (like `key` for API key and `auth` for an authentication token) we could do so in a generic way.

func MustParams(fn http.HandlerFunc, params ...string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
for _, param := range params {
if len(r.URL.Query().Get(param)) == 0 {
http.Error(w, "missing "+param, http.StatusBadRequest)
return
}
}
fn(w, r) // success - call handler
}

We can use MustParams to insist on different parameters by calling it with different arguments. The `params` argument inside MustParams will be captured in the closure of the function we return.

http.HandlerFunc("/user", MustParams(handleUser, "key", "auth"))
http.HandlerFunc("/group", MustParams(handleGroup, "key"))
http.HandlerFunc("/items", MustParams(handleItems, "key", "q"))

Wrappers within wrappers

Given the self-similar nature of this pattern, and the fact that we haven’t changed the `http.HandlerFunc` signature at all, we are able to easily chain wrappers as well as nest them in interesting ways.

If we have a `MustAuth` wrapper that will validate an auth token for a request, it might well insist on the `auth` parameter being present. So we can use the `MustParams` wrapper inside our `MustAuth` one:

func MustAuth(fn http.HandlerFunc) http.HandlerFunc {
return MustParams(func(w http.ResponseWriter, r *http.Request) {
    // TODO: use auth argument to validate request
    fn(w, r) // call original handler if successful
  }, "auth") // params argument to MustParams
}

The same idea works for database connections and other resources.

Changing the http.ResponseWriter

You may have noticed that `http.ResponseWriter` is an interface. This means it’s OK to swap it out for a different object, provided it satisfies the interface.

You might decide to do this if you want to log out what is written to the response using the wrapper pattern.

func log(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := NewResponseLogger(w)
fn(logger, r)
}
}

The `NewResponseLogger` above would create a type that intercepts calls to the `Write` function of the specified `http.ResponseWriter`, and logs the data out. Something like this:

func (r *ResponseLogger) Write(b []byte) (int, error) {
log.Print(string(b)) // log it out
return r.w.Write(b) // pass it to the original ResponseWriter
}

Real world examples

Here are some examples of how we have used wrapper functions with great results.

Which handler is being called?

Logging out “Before” and “After” might not be that useful, although adding the function name of the handler would allow it to act as a tracer:

func log(fn http.HandlerFunc) http.HandlerFunc {
name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
return func(w http.ResponseWriter, r *http.Request) {
log.Println("Before", name)
defer log.Println("After", name)
fn(w, r)
}
}

Now in the terminal or logs, we can see which function is being called:

Before handleThings
... log output from handleThings ...
After handleThings
Before handleDifferentThing
After handleDifferentThing

Of course, if we nest handlers or wrappers, this technique stops being useful — but it’s interesting to think of how our wrappers could be useful during development, which we might then remove before we release our code.

Common response headers

If we want to add a `X-App-Version` header to many API responses, we can easily do this by writing a `commonHeaders` wrapper that sets them before calling the original handler.

const appVersionStr = "1.0"
func commonHeaders(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-App-Version", appVersionStr)
fn(w, r)
}
}

Database session

Establishing and then cleaning up a database connection is a common utility of `http.HandlerFunc` wrappers.

If we have a `Server` type that will connect to MongoDB (via the excellent mgo package), we can use a wrapper variable to create a copy for each request.

Here’s our simple server:

type Server struct {
dbsession *mgo.Session
}
func NewServer() (*Server, error) {
dbsession, err := mgo.Dial("localhost")
if err != nil { return nil, err }
return &Server{dbsession:dbsession}, nil
}
func (s *Server) Close() {
s.dbsession.Close()
}

The `NewServer` function dials the database, and stores the `mgo.Session` in the `dbsession` field. The `Close` method will close the database session, and we’ll expect the calling code of our Server to call that for us (perhaps by deferring it when they create the server).

Now let’s add a wrapper called `WithData` to our `Server`, which is how we will indicate that we want our original handlers to have access to a database connection.

func (s *Server) WithData(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
dbcopy := s.dbsession.Copy()
defer dbcopy.Close()
context.Set(r, "db", dbcopy)
fn(w, r)
}
}

Our `WithData` method creates a `Copy` of the `mgo.Session` (and defers the clean-up `Close` call) and stores it in the context with the key “db”. We then call the original handler.

When we use our `handleThing` handler, we just call the `WithData` wrapper on the `Server` to indicate that we want the handler to be called with a configured database connection ready to go.

func main() {
srv, err := NewServer()
if err != nil {
// handle error
}
defer srv.Close() // close the server later
  // setup handlers
http.HandleFunc("/things", srv.WithData(handleThing))
  if err := http.ListenAndServer(":8080", nil) {
// handle error
}
// NOTE: this example doesn't call context.Clear and you should
}

Now, inside our handlers, we can access the database session and do something with it safe in the knowledge that we don’t have to worry about cleaning up after it:

func handleThing(w http.ResponseWriter, r *http.Request) {
db := context.Get(r, "db").(*mgo.Session)
err := db.DB("myapp").C("things").Insert(bson.M{"val":1})
if err != nil {
// handle error
}
io.WriteString(w, "Inserted")
}

You might even consider adding a more helpful getter function that could panic if the database connection is missing:

func db(r *http.Request) *mgo.Session {
db, ok := context.GetOk(r, "db")
if !ok {
panic("db missing: wrap with WithData")
}
return db
}

Finally, to prevent multiple sessions being copied by the WithData wrapper, you can use `context.GetOk` to first check to see if one has already been set or not. This is useful if you end up chaining and nesting wrappers, where you cannot guarantee each wrapper will only be called once. The WithData function would then look like this:

func (s *Server) WithData(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, present := context.GetOk(r, "db"); !present {
dbcopy := s.dbsession.Copy()
defer dbcopy.Close()
context.Set(r, "db", dbcopy)
}
fn(w, r)
}
}

Authentication

If our API design requires an `auth` parameter to be sent in requests to protected resources, we can use a wrapper to validate the token and load the user from a database and store it in the context.

We will assume our user objects in MongoDB look like this:

{
_id: "some ID",
name: "user's name",
auth: {
"apikey":"token",
"apikey2":"token"
}
}

The `auth` object is a map of API keys, to auth tokens. This allows an individual user to log in through different apps while keeping the auth tokens distinct.

We can model our User in Go code like this:

type User struct {
ID bson.ObjectId `bson:"_id"`
Name string `bson:"name"`
Auth map[string]string `bson:"auth"`
}

Our `WithAuth` wrapper will:

  • Validate the auth token
  • Use our `db` helper function to get a valid database connection
  • Query our `users` collection in MongoDB for a user that has the specified auth token for the specified API key
  • If no user was found, assume the keys are wrong and return with an http.StatusUnauthorized
  • If there are no errors, save the User object in the context for this request, and call the original handler

In code, this might look like this:

func WithAuth(fn http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
    // get and check the auth parameter
authtoken := r.URL.Query().Get("auth")
if len(authtoken) == 0 {
http.Error(w, "missing auth param", http.StatusUnauthorized)
return
}
    // assume key has already been validated
key := r.URL.Query().Get("key")
    // lookup the user
var user User
if err := db(r).DB("myapp").C("users").Find(bson.M{
"auth."+key: authtoken,
}).One(&user); err != nil {
      if err == mgo.ErrNotFound { // no user found
http.Error(w, "bad auth param", http.StatusUnauthorized)
return
}
      // some other error
http.Error(w, err.Error(), http.StatusInternalServerError)
return
    }
    // save the user in the context
context.Set(r, "user", &user)
    // call the original handler
fn(w, r)
  }
}

Conclusion

`http.HandlerFunc` wrappers let you do things before and/or after your original handler functions are called, providing pretty powerful middleware like capabilities using only the Go Standard Library.

Here’s a template wrapper for you to add to your #golang tool belt:

func MyWrapper(fn http.HandlerFunc) http.HandlerFunc {
// called once per wrapping
return func(w http.ResponseWriter, r *http.Request) {
    // called for each request
// defer clean-up
    fn(w, r) // call original
  }
}
Have you used http.HandlerFunc wrappers? If so, please share how in the comments below.

Go Programming Blueprints #shamelessplug





Learn more about the practicalities of Go with my book Go Programming Blueprints.


有疑问加站长微信联系(非本文作者)

本文来自:medium

感谢作者:Mat Ryer

查看原文:The http.HandlerFunc wrapper technique in #golang

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2146 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传