专栏首页GoUpUpGo 每日一库之 negroni

Go 每日一库之 negroni

简介

negroni是一个专注于 HTTP 中间件的库。它小巧,无侵入,鼓励使用标准库net/http的处理器(Handler)。本文就来介绍一下这个库。

为什么要使用中间件?有一些逻辑代码,如统计、日志、调试等,每一个处理器中都需要,如果一个个去添加太繁琐了、容易出错、容易遗漏。如果我们要统计处理器耗时,可以在每个处理器中添加代码统计耗时:

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

但是这个做法非常不灵活:

  • 每增加一个处理器,都需要添加这部分代码。而这些代码与实际的处理器逻辑并没有什么关系。编写处理器时比较容易遗忘,特别是要考虑所有的返回路径。增加了编码负担;
  • 不利于修改:如果统计代码有错误或者需要调整,必须要改动所有的处理器;
  • 添加麻烦:要添加其他的统计逻辑也需要改动所有的处理器代码。

利用 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()提供了几个常用的中间件:

  • 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

本文分享自微信公众号 - GoUpUp(GoUp-Up),作者:大俊

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Go 每日一库之 twirp

    twirp是一个基于 Google Protobuf 的 RPC 框架。twirp通过在.proto文件中定义服务,然后自动生产服务器和客户端的代码。让我们可以...

    用户7731323
  • Go 每日一库之 wire

    之前的一篇文章Go 每日一库之 dig介绍了 uber 开源的依赖注入框架dig。读了这篇文章后,@overtalk推荐了 Google 开源的wire工具。所...

    用户7731323
  • Go 每日一库之 go-cmp

    我们时常有比较两个值是否相等的需求,最直接的方式就是使用==操作符,其实==的细节远比你想象的多,我在**深入理解 Go 之==**中有详细介绍,有兴趣去看看。...

    用户7731323
  • Golang 下载文件

    这里使用net/http下载文件,可以设置Content-Type(具体参考)告诉客户端返回的内容类型实际上是什么,实现不同方式的文件的下载。

    孤烟
  • Go语言经典库使用分析(四)| Gorilla Handlers 源代码实现分析

    上一篇 Go语言经典库使用分析(三)| Gorilla Handlers 详细介绍 中介绍了Handlers常用中间件的使用,这一篇介绍下这些中间件实现的原理...

    飞雪无情
  • 真正“搞”懂http协议01—背景故事

    zaking
  • Nodejs创建http客户端及代理服务器

    nodejs除了可以通过http模块创建服务器,还能创建客户端,类似于浏览器那样很轻松的去向别的服务器发送请求并获取响应数据。

    前端_AWhile
  • 深度学习数据集(一)

    海量数据(又称大数据)已经成为各大互联网企业面临的最大问题,如何处理海量数据,提供更好的解决方案,是目前相当热门的一个话题。类似MapReduce、 Hadoo...

    深度学习思考者
  • Go 使用标准库 net/http 包构建服务器

    在 Go 语言中,使用标准库 net/http 可以很方便的构建服务器,只要调用 ListenAndServe 函数,并传入参数IP地址与端口组成的字符串和处理...

    frankphper
  • Prometheus 整合 AlertManager

    Alertmanager 主要用于接收 Prometheus 发送的告警信息,它很容易做到告警信息的去重,降噪,分组,策略路由,是一款前卫的告警通知系统。它支持...

    程序员果果

扫码关注云+社区

领取腾讯云代金券