Go 每日一庫之 negroni

簡介

negroni是一個專一於 HTTP 中間件的庫。它小巧,無侵入,鼓勵使用標準庫net/http的處理器(Handler)。本文就來介紹一下這個庫。git

爲何要使用中間件?有一些邏輯代碼,如統計、日誌、調試等,每個處理器中都須要,若是一個個去添加太繁瑣了、容易出錯、容易遺漏。若是咱們要統計處理器耗時,能夠在每一個處理器中添加代碼統計耗時:github

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)
}

可是這個作法很是不靈活:golang

  • 每增長一個處理器,都須要添加這部分代碼。而這些代碼與實際的處理器邏輯並無什麼關係。編寫處理器時比較容易遺忘,特別是要考慮全部的返回路徑。增長了編碼負擔;
  • 不利於修改:若是統計代碼有錯誤或者須要調整,必需要改動全部的處理器;
  • 添加麻煩:要添加其餘的統計邏輯也須要改動全部的處理器代碼。

利用 Go 語言的閉包,咱們能夠將實際的處理器代碼封裝到一個函數中,在這個函數中執行額外的邏輯:瀏覽器

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)
}

咱們將額外的與處理器無關的代碼放在另外的函數中。註冊處理器函數時,咱們不直接使用原始的處理器函數,而是用elasped函數封裝一層。實際上elasped這樣的函數就是中間件。它封裝原始的處理器函數,返回一個新的處理器函數。從而能很方便在實際的處理邏輯先後插入代碼,便於添加、修改和維護。服務器

快速使用

先安裝:微信

$ go get github.com/urfave/negroni

後使用:閉包

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)
}

negroni的使用很是簡單,它能夠很方便的與http.Handler一塊兒使用。negroni.Classic()提供了幾個經常使用的中間件:dom

  • negroni.Recovery:恢復panic,處理器代碼中有panic會被這個中間件捕獲,程序不會退出;
  • negroni.Logger:日誌,記錄請求和響應的基本信息;
  • negroni.Static:在public目錄提供靜態文件服務。

調用n.UseHandler(mux),將這些中間件應用到多路複用器上。運行,在瀏覽器中輸入localhost:3000,查看控制檯輸出:函數

$ 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

接口negroni.Handler讓咱們對中間件的執行流程有更靈活的控制:學習

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

咱們編寫的中間件簽名必須是func(http.ResponseWriter,*http.Request,http.HandlerFunc),或者實現negroni.Handler接口:

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)
}

上面代碼中實現了一個隨機的中間件,有一半的機率直接從RandomMiddleware這個中間件返回,一半的機率執行實際的處理器函數。運行程序,在瀏覽器中不停地刷新頁面localhost:3000看看效果。

注意,實際上func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)只是一個方便的寫法。在調用n.Use時使用了negroni.HandlerFunc作了一層封裝,而negroni.HandlerFunc實現了negroni.Handler接口:

// 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/http中也有相似的代碼,經過http.HandlerFunc封裝func(http.ResponseWriter,*http.Request)從而實現接口http.Handler

negroni.With

若是有多箇中間件,每一個都須要n.Use()有些繁瑣。negroni提供了一個With()方法,它接受一個或多個negroni.Handler參數,返回一個新的對象:

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

Negroni對象提供了一個方便的Run()方法來運行服務器程序。它接受與http.ListenAndServe()同樣的地址(Addr)參數:

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")
}

若是未指定端口,那麼嘗試使用PORT環境變量。若是PORT環境變量也未設置,那麼使用默認的端口:8080

做爲http.Handler使用

negroni很容易在net/http程序中使用,negroni.Negroni對象可直接做爲http.Handler傳給相應的方法:

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()
}

內置中間件

negroni內置了一些經常使用的中間件,可直接使用。

Static

negroni.Static可在指定目錄中提供文件服務:

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)
}

在程序運行目錄下建立public目錄,而後放入一些文件1.txt2.jpg。程序運行以後,就能經過瀏覽器localhost:3000/1.txtlocalhost:3000/2.jpg請求這些文件了。

另外須要特別注意一點,若是找不到對應的文件,Static會將請求傳給下一個中間件或處理器函數。在上面的例子中就是hello world。在瀏覽器中輸入localhost:3000/none-exist.txt看看效果。

Logger

在快速開始中,咱們經過negroni.Classic()已經使用過這個中間件了。咱們也能夠單獨使用,它能夠記錄請求的信息。咱們還能夠調用SetFormat()方法設置日誌的格式:

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)
}

上面代碼中將日誌格式設置爲[{{.Status}} {{.Duration}}] - {{.Request.UserAgent}},即響應狀態、耗時和UserAgent

使用 Chrome 瀏覽器請求:

[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.Recovery能夠捕獲後續的中間件或處理器函數中出現的panic,返回一個500的響應碼:

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)
}

請求時panic的堆棧會顯示在瀏覽器中:

這在開發環境比較有用,可是生成環境中不能泄露這個信息。這時能夠設置PrintStack字段爲false

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)
}

除了在控制檯和瀏覽器中輸出panic信息,Recovery還提供了鉤子函數,能夠向其餘服務上報panic,如Sentry/Airbrake。固然上報的代碼要本身寫😄。

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")
}

設置PanicHandlerFunc以後,發生panic就會調用此函數。

咱們還能夠對輸出的格式進行設置,設置Formatter字段爲negroni.HTMLPanicFormatter能讓輸出更好地在瀏覽器中呈現:

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)
}

效果:

第三方中間件

除了內置中間件外,negroni還有不少第三方的中間件。完整列表看這裏:https://github.com/urfave/negroni#third-party-middleware

咱們只介紹一個xrequestid,它在每一個請求中增長一個隨機的HeaderX-Request-Id

安裝xrequestid

$ go get github.com/pilu/xrequestid

使用:

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")
}

給每一個請求增長一個 16 字節的X-Request-Id,處理器函數中將這個X-Request-Id寫入響應中,最後呈如今瀏覽器中。運行程序,在瀏覽器中輸入localhost:3000查看效果。

總結

negroni專一於中間件,沒有不少花哨的功能。無侵入性使得它很容易與標準庫net/http和其餘的 Web 庫(如gorilla/mux)一塊兒使用。

你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄

參考

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

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索