Go Negroni

Time:2021-4-14

brief introduction

negroniIs a library that focuses on HTTP middleware. It is small, non intrusive and encourages the use of standard librariesnet/httpProcessor for(Handler)。 This article introduces the library.

Why middleware? There are some logic codes, such as statistics, logs, debugging, etc., which are needed in every processor. If you add them one by one, they are too cumbersome, error prone, and easy to miss. If we want to count processor time, we can add code to each processor to count time

package main

import (
  "fmt"
  "net/http"
  "time"
)

func index(w http.ResponseWriter, r *http.Request) {
  start := time.Now()
  fmt.Fprintf(w, "home page")
  fmt.Printf("index elasped:%fs", time.Since(start).Seconds())
}

func greeting(w http.ResponseWriter, r *http.Request) {
  start := time.Now()
  name := r.FormValue("name")
  if name == "" {
    name = "world"
  }

  fmt.Fprintf(w, "hello %s", name)
  fmt.Printf("greeting elasped:%fs\n", time.Since(start).Seconds())
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", index)
  mux.HandleFunc("/greeting", greeting)

  http.ListenAndServe(":8000", mux)
}

But this is very inflexible:

  • Every time you add a processor, you need to add this part of the code. This code has nothing to do with the actual processor logic. It’s easy to forget when writing processor, especially considering all return paths. It increases the coding burden;
  • Not conducive to modification: if the statistical code has errors or needs to be adjusted, all processors must be modified;
  • All the other things that need to be changed to add logic to the processor are also troublesome.

Using the closure of go language, we can encapsulate the actual processor code into a function, in which additional logic is executed

func elasped(h func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Path
    start := time.Now()
    h(w, r)
    fmt.Printf("path:%s elasped:%fs\n", path, time.Since(start).Seconds())
  }
}

func index(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "home page")
}

func greeting(w http.ResponseWriter, r *http.Request) {
  name := r.FormValue("name")
  if name == "" {
    name = "world"
  }

  fmt.Fprintf(w, "hello %s", name)
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", elasped(index))
  mux.HandleFunc("/greeting", elasped(greeting))

  http.ListenAndServe(":8000", mux)
}

We put extra processor independent code in another function. When registering processor functions, we do not use the original processor functions directly, but use theelaspedFunction encapsulates a layer. actuallyelaspedSuch a function is middleware. It encapsulates the original processor function and returns a new one. So it is very convenient to insert code before and after the actual processing logic, easy to add, modify and maintain.

Quick use

Install first:

$ go get github.com/urfave/negroni

After use:

package main

import (
  "fmt"
  "net/http"

  "github.com/urfave/negroni"
)

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
  })

  n := negroni.Classic()
  n.UseHandler(mux)

  http.ListenAndServe(":3000", n)
}

negroniIt’s very easy to use, it can be very convenient withhttp.HandlerUsed together.negroni.Classic()Several common middleware are provided

  • negroni.Recovery: recoverypanic, in the processor codepanicIt will be captured by the middleware and the program will not exit;
  • negroni.Logger: log to record the basic information of request and response;
  • negroni.Static: inpublicDirectory provides static file service.

calln.UseHandler(mux)The middleware is applied to the multiplexer. Run, input in browserlocalhost:3000To view the console output:

$ go run main.go 
[negroni] 2020-06-22T06:48:53+08:00 | 200 |      20.9966ms | localhost:3000 | GET /
[negroni] 2020-06-22T06:48:54+08:00 | 200 |      0s | localhost:3000 | GET /favicon.ico

negroni.Handler

Interfacenegroni.HandlerLet’s have more flexible control over the execution process of middleware

type Handler interface {
  ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
}

The middleware signature we write must befunc(http.ResponseWriter,*http.Request,http.HandlerFunc)Or implementationnegroni.HandlerInterface:

func RandomMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  if rand.Int31n(100) <= 50 {
    fmt.Fprintf(w, "hello from RandomMiddleware")
  } else {
    next(w, r)
  }
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
  })

  n := negroni.New()
  n.Use(negroni.HandlerFunc(RandomMiddleware))
  n.UseHandler(mux)

  http.ListenAndServe(":3000", n)
}

In the above code, a random middleware is implemented, with half the probability of being directly from theRandomMiddlewareThe middleware returns half the probability of executing the actual processor function. Run the program and refresh the page continuously in the browserlocalhost:3000Look at the effect.

Notice, actuallyfunc(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)It’s just a convenient way to write. When callingn.UseWe used thenegroni.HandlerFuncA layer of encapsulation is made, andnegroni.HandlerFuncRealizednegroni.HandlerInterface:

// src/github.com/urfave/negroni/negroni.go
type HandlerFunc func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)

func (h HandlerFunc) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  h(rw, r, next)
}

net/httpThere is a similar code inhttp.HandlerFuncencapsulationfunc(http.ResponseWriter,*http.Request)So as to realize the interfacehttp.Handler

negroni.With

If there are multiple middleware, each needs ton.Use()It’s a little cumbersome.negroniProvides aWith()Method, which accepts one or morenegroni.HandlerParameter to return a new object:

func Middleware1(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  fmt.Println("Middleware1 begin")
  next(w, r)
  fmt.Println("Middleware1 end")
}

func Middleware2(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  fmt.Println("Middleware2 begin")
  next(w, r)
  fmt.Println("Middleware2 end")
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
  })

  n := negroni.New()
  n = n.With(
    negroni.HandlerFunc(Middleware1),
    negroni.HandlerFunc(Middleware2),
  )
  n.UseHandler(mux)

  http.ListenAndServe(":3000", n)
}

Run

NegroniObject provides a convenientRun()Method to run the server program. It accepts andhttp.ListenAndServe()Same address(Addr)Parameters:

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
  })

  n := negroni.New()
  n.UseHandler(mux)
  n.Run(":3000")
}

If no port is specified, try usingPORTEnvironment variables. IfPORTIf the environment variable is not set, the default port is used:8080

Ashttp.Handleruse

negroniIt’s easy to be innet/httpUsed in the program,negroni.NegroniObject can be used directly ashttp.HandlerTo the corresponding method:

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World!")
  })

  n := negroni.Classic()
  n.UseHandler(mux)

  s := &http.Server{
    Addr:           ":8080",
    Handler:        n,
    ReadTimeout:    10 * time.Second,
    WriteTimeout:   10 * time.Second,
    MaxHeaderBytes: 1 << 20,
  }
  s.ListenAndServe()
}

Built in middleware

negroniBuilt in some common middleware, can be used directly.

Static

negroni.StaticFile service can be provided in the specified directory:

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello world")
  })

  n := negroni.New()
  n.Use(negroni.NewStatic(http.Dir("./public")))
  n.UseHandler(mux)

  http.ListenAndServe(":3000", n)
}

Create in the program running directorypublicDirectory, and then put in some files1.txt2.jpg. After the program runs, it can be accessed through the browserlocalhost:3000/1.txtandlocalhost:3000/2.jpgThe documents are requested.

In addition, we need to pay special attention to the fact that if the corresponding file cannot be found,StaticThe request is passed to the next middleware or processor function. In the above examplehello world. Input in browserlocalhost:3000/none-exist.txtLook at the effect.

Logger

In the quick start, we go through thenegroni.Classic()This middleware has been used. We can also use it alone, it can record the requested information. We can also callSetFormat()Method to format the log:

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello world")
  })

  n := negroni.New()
  logger := negroni.NewLogger()
  logger.SetFormat("[{{.Status}} {{.Duration}}] - {{.Request.UserAgent}}")
  n.Use(logger)
  n.UseHandler(mux)

  http.ListenAndServe(":3000", n)
}

The above code sets the log format to[{{.Status}} {{.Duration}}] - {{.Request.UserAgent}}, i.e. response status, time consuming andUserAgent

Use Chrome browser to request:

[negroni] [200 26.0029ms] - Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36

Recovery

negroni.RecoveryIt can capture the data that appear in subsequent middleware or processor functionspanic, returns a500Response code:

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    panic("internal server error")
  })

  n := negroni.New()
  n.Use(negroni.NewRecovery())
  n.UseHandler(mux)

  http.ListenAndServe(":3000", n)
}

On requestpanicThe stack of is displayed in the browser:

Go Negroni

This is useful in the development environment, but you can’t leak this information in the build environment. This can be setPrintStackField isfalse

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    panic("internal server error")
  })

  n := negroni.New()
  r := negroni.NewRecovery()
  r.PrintStack = false
  n.Use(r)
  n.UseHandler(mux)

  http.ListenAndServe(":3000", n)
}

In addition to output in the console and browserpanicInformation,RecoveryHook function is also provided to report to other servicespanic, such asSentry/Airbrake. Of course, you should write your own code.

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    panic("internal server error")
  })

  n := negroni.New()
  r := negroni.NewRecovery()
  r.PanicHandlerFunc = reportToSentry
  n.Use(r)
  n.UseHandler(mux)

  http.ListenAndServe(":3000", n)
}

func reportToSentry(info *negroni.PanicInformation) {
  fmt.Println("sent to sentry")
}

set upPanicHandlerFuncAfter that, it happenedpanicThis function is called.

We can also set the format of the outputFormatterField isnegroni.HTMLPanicFormatterBetter rendering of output in browser:

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    panic("internal server error")
  })

  n := negroni.New()
  r := negroni.NewRecovery()
  r.Formatter = &negroni.HTMLPanicFormatter{}
  n.Use(r)
  n.UseHandler(mux)

  http.ListenAndServe(":3000", n)
}

effect:

Go Negroni

Third party Middleware

In addition to the built-in middleware,negroniThere are also many third-party middleware. Here is a complete list:https://github.com/urfave/negroni#third-party-middleware

Let’s just introduce onexrequestid, which adds a randomHeaderX-Request-Id

installxrequestid

$ go get github.com/pilu/xrequestid

use:

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "X-Request-Id is `%s`", r.Header.Get("X-Request-Id"))
  })

  n := negroni.New()
  n.Use(xrequestid.New(16))
  n.UseHandler(mux)
  n.Run(":3000")
}

Add a 16 byte prefix to each requestX-Request-IdIn the processor functionX-Request-IdWrite the response, and finally present it in the browser. Run the program and input in the browserlocalhost:3000See the effect.

summary

negroniFocus on middleware, not many fancy functions. Noninvasive makes it easy to interact with standard librariesnet/httpAnd other web libraries (such asgorilla/mux)Used together.

If you find a fun and useful go language library, you are welcome to submit an issue on GitHub, the go daily library

reference resources

  1. negroni GitHub:https://github.com/urfave/negroni
  2. Go daily GitHub:https://github.com/darjun/go-daily-lib

I

My blog:https://darjun.github.io

Welcome to my WeChat official account, GoUpUp, learn together and make progress together.

Go Negroni