Go语言的Http 中间件实现

本文涉及的产品
注册配置 MSE Nacos/ZooKeeper,118元/月
性能测试 PTS,5000VUM额度
云原生网关 MSE Higress,422元/月
简介:

当你正在构建一个Web应用程序有可能要运行许多(甚至全部)的HTTP请求一些共享功能,你可能想记录每一个request,gzip压缩的每个response,或者做一些繁重的处理或者缓存检查。

实现这个共享功能的一种方法是将其设置为中间件,他可以作为一个独立的程序,在正常的handlers处理之前。根本不需要重写代码:如果你想用一个中间件,就把它加上应用中;如果你改变主意了,去掉就好了。就这么简单。


 
 
  1. ServeMux => Middleware Handler => Application Handler 

这篇文章,我会给大家介绍怎么自己去实现一个自定义的middleware模式。以及通过使用第三方的中间件软件包的一些具体的实例。

基本原则:

在Go语言中实现和使用middleware是非常简单的。

使我们的中间件能搞满足 http.handlers 这个接口

建立一个 handlers 链,使其能够满足中间件的 handler 和 正常应用的 handler,并且能够注册到 http.ServeMux

我来解释如何实现:

首先你要知道go 的http handle,这里假设你是知道的


 
 
  1. func messageHandler(message string) http.Handler { 
  2.  
  3. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
  4.  
  5. w.Write([]byte(message) 
  6.  
  7. }) 
  8.  

这上面这个代码片段里面我们的逻辑很简单只是一个简单的 w.Write() 然后我们使用 http.HandlerFunc 适配器来转化这个闭包,并返回。

我们可以使用一个相同的方法来创建一个 handler 链。可以使用 handler 代替参数 string 传进闭包,然后把控制 handler 给传进来的 handler,并且调用 ServeHTTP() 方法。

这给了我们一个完整的模式构建中间件:


 
 
  1. func exampleMiddleware(next http.Handler) http.Handler { 
  2.  
  3. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
  4.  
  5. // Our middleware logic goes here... 
  6.  
  7. next.ServeHTTP(w, r) 
  8.  
  9. }) 
  10.  

你注意到这个中间件有一个这样的函数结构 func(http.Handler) http.Handler 。它接受一个 handler 作为参数,并且返回一个 handler。这里有两个很有用的原因:

因为这个函数返回一个句柄可以直接供中间件注册

我们可以建立任意长度的 handler 链来通过中间件的方法互相嵌套

比如:


 
 
  1. http.Handle("/", middlewareOne(middlewareTwo(finalHandler))) 

控制流说明:

让我们来看一个带有多个中间件的例子,并且把日志输出到控制台:


 
 
  1. package main 
  2.  
  3. import ( 
  4.  
  5. "log" 
  6.  
  7. "net/http" 
  8.  
  9.  
  10. func middlewareOne(next http.Handler) http.Handler { 
  11.  
  12. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
  13.  
  14. log.Println("Executing middlewareOne"
  15.  
  16. next.ServeHTTP(w, r) 
  17.  
  18. log.Println("Executing middlewareOne again"
  19.  
  20. }) 
  21.  
  22.  
  23. func middlewareTwo(next http.Handler) http.Handler { 
  24.  
  25. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
  26.  
  27. log.Println("Executing middlewareTwo"
  28.  
  29. if r.URL.Path != "/" { 
  30.  
  31. return 
  32.  
  33.  
  34. next.ServeHTTP(w, r) 
  35.  
  36. log.Println("Executing middlewareTwo again"
  37.  
  38. }) 
  39.  
  40.  
  41. func final(w http.ResponseWriter, r *http.Request) { 
  42.  
  43. log.Println("Executing finalHandler"
  44.  
  45. w.Write([]byte("OK")) 
  46.  
  47.  
  48. func main() { 
  49.  
  50. finalHandler := http.HandlerFunc(final
  51.  
  52. http.Handle("/", middlewareOne(middlewareTwo(finalHandler))) 
  53.  
  54. http.ListenAndServe(":3000", nil) 
  55.  

然后我们执行 go run main.go 在浏览器打开http://localhost:3000。 你会看到下面的输出。

4626AADF-4508-4094-B16B-59356D45FB55

我们能够很清楚的看到handle的流程控制。我们嵌套他们的返回顺序。我们可以通过中间件中得 return 随时停止handle链的控制。

在上面的代码中我们在middlewareTwo function包含了retrun 语句。我们在浏览器中打开http://localhost:3000/foo,我们会看到。


 
 
  1. 2015/12/19 04:21:57 Executing middlewareOne 
  2.  
  3. 2015/12/19 04:21:57 Executing middlewareTwo 
  4.  
  5. 2015/12/19 04:21:57 Executing middlewareOne again 
  6.  
  7. 2015/12/19 04:21:57 Executing middlewareOne 
  8.  
  9. 2015/12/19 04:21:57 Executing middlewareTwo 
  10.  
  11. 2015/12/19 04:21:57 Executing middlewareOne again 

我们实现一个真实的项目的示例:

我们实现一个判断请求是不是XMl的功能,我们要实现一个中间件。用来检查的请求体的存在。检查请求体,以确保它是XML。如果其中检查失败,我希望我们的中间件输出错误信息然后终止我们的handle处理。


 
 
  1. package main 
  2.  
  3. import ( 
  4.  
  5. "bytes" 
  6.  
  7. "net/http" 
  8.  
  9.  
  10. func enforceXMLHandler(next http.Handler) http.Handler { 
  11.  
  12. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
  13.  
  14. // Check for a request body 
  15.  
  16. if r.ContentLength == 0 { 
  17.  
  18. http.Error(w, http.StatusText(400), 400) 
  19.  
  20. return 
  21.  
  22.  
  23. // Check its MIME type 
  24.  
  25. buf := new(bytes.Buffer) 
  26.  
  27. buf.ReadFrom(r.Body) 
  28.  
  29. if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" { 
  30.  
  31. http.Error(w, http.StatusText(415), 415) 
  32.  
  33. return 
  34.  
  35.  
  36. next.ServeHTTP(w, r) 
  37.  
  38. }) 
  39.  
  40.  
  41. func main() { 
  42.  
  43. finalHandler := http.HandlerFunc(final
  44.  
  45. http.Handle("/", enforceXMLHandler(finalHandler)) 
  46.  
  47. http.ListenAndServe(":3000", nil) 
  48.  
  49.  
  50. func final(w http.ResponseWriter, r *http.Request) { 
  51.  
  52. w.Write([]byte("OK")) 
  53.  

为了检验我们的中间件是否实现了这个功能,我们首先创建一个XML文件。


 
 
  1. $ cat > books.xml  
  2. H. G. Wells  
  3. 8.50   

然后通过使用cURL来进行模拟请求:


 
 
  1. $ curl -i localhost:3000 
  2.  
  3. HTTP/1.1 400 Bad Request 
  4.  
  5. Content-Type: text/plain; charset=utf-8 
  6.  
  7. Content-Length: 12 
  8.  
  9. Bad Request 
  10.  
  11. $ curl -i -d "This is not XML" localhost:3000 
  12.  
  13. HTTP/1.1 415 Unsupported Media Type 
  14.  
  15. Content-Type: text/plain; charset=utf-8 
  16.  
  17. Content-Length: 23 
  18.  
  19. Unsupported Media Type 
  20.  
  21. $ curl -i -d @books.xml localhost:3000 
  22.  
  23. HTTP/1.1 200 OK 
  24.  
  25. Date: Fri, 17 Oct 2014 13:42:10 GMT 
  26.  
  27. Content-Length: 2 
  28.  
  29. Content-Type: text/plain; charset=utf-8 
  30.  
  31. OK 

接下来给大家介绍一下第三方中间件的使用:

秉承不造轮子的原则,其实在Github上有很多实现了一些功能的中间件。比如这里给大家介绍2个基础验证的中间件goji/httpauth和Gorilla’s LoggingHandler

首先我们需要引入第三方包


 
 
  1. $ go get github.com/goji/httpauth 

 
 
  1. package main 
  2.  
  3. import ( 
  4.  
  5. "github.com/goji/httpauth" 
  6.  
  7. "net/http" 
  8.  
  9.  
  10. func main() { 
  11.  
  12. finalHandler := http.HandlerFunc(final
  13.  
  14. authHandler := httpauth.SimpleBasicAuth("username""password"
  15.  
  16. http.Handle("/", authHandler(finalHandler)) 
  17.  
  18. http.ListenAndServe(":3000", nil) 
  19.  
  20.  
  21. func final(w http.ResponseWriter, r *http.Request) { 
  22.  
  23. w.Write([]byte("OK")) 
  24.  

如果你运行这个例子,你应该得到你所期望的有效和无效的凭证响应


 
 
  1. $ curl -i username:password@localhost:3000 
  2.  
  3. HTTP/1.1 200 OK 
  4.  
  5. Content-Length: 2 
  6.  
  7. Content-Type: text/plain; charset=utf-8 
  8.  
  9. OK 
  10.  
  11. $ curl -i username:wrongpassword@localhost:3000 
  12.  
  13. HTTP/1.1 401 Unauthorized 
  14.  
  15. Content-Type: text/plain; charset=utf-8 
  16.  
  17. Www-Authenticate: Basic realm=""Restricted"" 
  18.  
  19. Content-Length: 13 
  20.  
  21. Unauthorized 

Gorilla’s LoggingHandler和Apache-style logs有一些区别

以下是我们在其中写入日志到server.log文件一个简单的例子:

首先还是引入第三包


 
 
  1. go get github.com/gorilla/handlers 

 
 
  1. package main 
  2.  
  3. import ( 
  4.  
  5. "github.com/gorilla/handlers" 
  6.  
  7. "net/http" 
  8.  
  9. "os" 
  10.  
  11.  
  12. func main() { 
  13.  
  14. finalHandler := http.HandlerFunc(final
  15.  
  16. logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 
  17.  
  18. if err != nil { 
  19.  
  20. panic(err) 
  21.  
  22.  
  23. http.Handle("/", handlers.LoggingHandler(logFile, finalHandler)) 
  24.  
  25. http.ListenAndServe(":3000", nil) 
  26.  
  27.  
  28. func final(w http.ResponseWriter, r *http.Request) { 
  29.  
  30. w.Write([]byte("OK")) 
  31.  

在一个简单的情况下,这样我们的代码是相当清楚的。但是,如果我们想用LoggingHandler作为一个更大的中间件链中的一部分会发生什么?我们可以很容易地结束了一个声明,看起来像这样:


 
 
  1. http.Handle("/", handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler)))) 

不过这看起来太糟糕了。

我们可以通过创建一个构造函数打来整理一下我们给它取名为(myLoggingHandler)

和signature func(http.Handler) http.Handler.这样就会是我们的代码更加整洁和可读性:


 
 
  1. func myLoggingHandler(h http.Handler) http.Handler { 
  2.  
  3. logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 
  4.  
  5. if err != nil { 
  6.  
  7. panic(err) 
  8.  
  9.  
  10. return handlers.LoggingHandler(logFile, h) 
  11.  
  12.  
  13. func main() { 
  14.  
  15. finalHandler := http.HandlerFunc(final
  16.  
  17. http.Handle("/", myLoggingHandler(finalHandler)) 
  18.  
  19. http.ListenAndServe(":3000", nil) 
  20.  

 
 
  1. $ cat server.log 
  2.  
  3. 127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "GET / HTTP/1.1" 200 2 
  4.  
  5. 127.0.0.1 - - [21/Oct/2014:18:56:36 +0100] "POST / HTTP/1.1" 200 2 
  6.  
  7. 127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "PUT / HTTP/1.1" 200 2 

这里还有一个比较完整结构的中间件使用的示例:


 
 
  1. package main 
  2.  
  3. import ( 
  4.  
  5. "bytes" 
  6.  
  7. "github.com/goji/httpauth" 
  8.  
  9. "github.com/gorilla/handlers" 
  10.  
  11. "net/http" 
  12.  
  13. "os" 
  14.  
  15.  
  16. func enforceXMLHandler(next http.Handler) http.Handler { 
  17.  
  18. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 
  19.  
  20. if r.ContentLength == 0 { 
  21.  
  22. http.Error(w, http.StatusText(400), 400) 
  23.  
  24. return 
  25.  
  26.  
  27. buf := new(bytes.Buffer) 
  28.  
  29. buf.ReadFrom(r.Body) 
  30.  
  31. if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" { 
  32.  
  33. http.Error(w, http.StatusText(415), 415) 
  34.  
  35. return 
  36.  
  37.  
  38. next.ServeHTTP(w, r) 
  39.  
  40. }) 
  41.  
  42.  
  43. func myLoggingHandler(h http.Handler) http.Handler { 
  44.  
  45. logFile, err := os.OpenFile("server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 
  46.  
  47. if err != nil { 
  48.  
  49. panic(err) 
  50.  
  51.  
  52. return handlers.LoggingHandler(logFile, h) 
  53.  
  54.  
  55. func main() { 
  56.  
  57. indexHandler := http.HandlerFunc(index) 
  58.  
  59. authHandler := httpauth.SimpleBasicAuth("username""password"
  60.  
  61. http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(indexHandler)))) 
  62.  
  63. http.ListenAndServe(":3000", nil) 
  64.  
  65.  
  66. func index(w http.ResponseWriter, r *http.Request) { 
  67.  
  68. w.Write([]byte("OK")) 
  69.  

有很多人不太喜欢中间件的设计模式,不过我还是慢喜欢的。


作者:何妍 

来源:51CTO

相关文章
|
6天前
|
存储 JSON 监控
Viper,一个Go语言配置管理神器!
Viper 是一个功能强大的 Go 语言配置管理库,支持从多种来源读取配置,包括文件、环境变量、远程配置中心等。本文详细介绍了 Viper 的核心特性和使用方法,包括从本地 YAML 文件和 Consul 远程配置中心读取配置的示例。Viper 的多来源配置、动态配置和轻松集成特性使其成为管理复杂应用配置的理想选择。
23 2
|
4天前
|
Go 索引
go语言中的循环语句
【11月更文挑战第4天】
13 2
|
4天前
|
Go C++
go语言中的条件语句
【11月更文挑战第4天】
16 2
|
8天前
|
程序员 Go
go语言中的控制结构
【11月更文挑战第3天】
85 58
|
7天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
4天前
|
Go
go语言中的 跳转语句
【11月更文挑战第4天】
13 4
|
4天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
18 1
|
8天前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
8天前
|
Go 数据处理 调度
探索Go语言的并发模型:Goroutines与Channels的协同工作
在现代编程语言中,Go语言以其独特的并发模型脱颖而出。本文将深入探讨Go语言中的Goroutines和Channels,这两种机制如何协同工作以实现高效的并发处理。我们将通过实际代码示例,展示如何在Go程序中创建和管理Goroutines,以及如何使用Channels进行Goroutines之间的通信。此外,本文还将讨论在使用这些并发工具时可能遇到的常见问题及其解决方案,旨在为Go语言开发者提供一个全面的并发编程指南。
|
6天前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。