有了net/http, 为什么还要有gin

1. 简介

在Go语言中,net/http 包提供了一个强大且灵活的标准HTTP库,可以用来构建Web应用程序和处理HTTP请求。这个包是Go语言标准库的一部分,因此所有的Go程序都可以直接使用它。既然已经有 net/http 这样强大和灵活的标准库,为什么还出现了像 Gin 这样的,方便我们构建Web应用程序的第三方库?

其实在于net/http的定位,其提供了基本的HTTP功能,但它的设计目标是简单和通用性,而不是提供高级特性和便利的开发体验。在处理HTTP请求和构建Web应用时,可能会遇到一系列的问题,这也造就了Gin 这样的第三方库的出现。

下文我们将对一系列场景的介绍,通过比对 net/httpGin 二者在这些场景下的不同实现,进而说明Gin 框架存在的必要性。

2. 复杂路由场景处理

在实际的Web应用程序开发中,使用同一个路由前缀的场景非常普遍,这里举两个比较常见的例子。

比如在设计API时,可能会随着时间的推移对API进行更新和改进。为了保持向后兼容性,并允许多个API版本共存,通常会使用类似 /v1/v2 这样的路由前缀来区分不同版本的API。

还有另外一个场景,一个大型Web应用程序经常是由多个模块组成,每个模块负责不同的功能。为了更好地组织代码和区分不同模块的路由,经常都是使用模块名作为路由前缀。

在这两个场景中,大概率都会使用同一个路由前缀。如果使用net/http 来框架web应用,实现大概如下:

package main

import (
        "fmt"
        "net/http"
)

func handleUsersV1(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "User list in v1")
}

func handlePostsV1(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Post list in v1")
}

func main() {
        http.HandleFunc("/v1/users", handleUsersV1)
        http.HandleFunc("/v1/posts", handlePostsV1)
        http.ListenAndServe(":8080", nil)
}

在上面的示例中,我们手动使用 http.HandleFunc 来定义不同的路由处理函数。

代码示例看起来没有太大问题,但是是因为只有两个路由组,如果随着路由数量增加,处理函数的数量也会增加,代码会变得越来越复杂和冗长。而且每一个路由规则都需要手动设置路由前缀,如例子中的 v1 前缀,如果前缀是 /v1/v2/... 这样子设置起来,会导致代码架构不清晰,同时操作繁杂,容易出错。

但是相比之下,Gin 框架实现了路由分组的功能,下面来看Gin 框架来对该功能的实现:

package main

import (
        "fmt"
        "github.com/gin-gonic/gin"
)

func main() {
        router := gin.Default()
        // 创建一个路由组
        v1 := router.Group("/v1")
        {
                v1.GET("/users", func(c *gin.Context) {
                        c.String(200, "User list in v1")
                })
                v1.GET("/posts", func(c *gin.Context) {
                        c.String(200, "Post list in v1")
                })
        }
        router.Run(":8080")
}

在上面的例子中,通过router.Group 创建了一个v1 路由前缀的路由组,我们设置路由规则时,不需要再设置路由前缀,框架会自动帮我们组装好。

同时,相同路由前缀的规则,也在同一个代码块里进行维护。 相比于 net/http 代码库,Gin 使得代码结构更清晰、更易于管理。

3. 中间件处理

在web应用请求处理过程中,除了执行具体的业务逻辑之外,往往需要在这之前执行一些通用的逻辑,比如鉴权操作,错误处理或者是日志打印功能,这些逻辑我们统称为中间件处理逻辑,而且往往是必不可少的。

首先对于错误处理,在应用程序的执行过程中,可能会发生一些内部错误,如数据库连接失败、文件读取错误等。合理的错误处理可以避免这些错误导致整个应用崩溃,而是通过适当的错误响应告知客户端。

对于鉴权操作,在许多web处理场景中,经常都是用户认证之后,才能访问某些受限资源或执行某些操作。同时鉴权操作还可以限制用户的权限,避免用户有未经授权的访问,这有助于提高程序的安全性。

因此,一个完整的HTTP请求处理逻辑,是极有可能需要这些中间件处理逻辑的。而且理论上框架或者类库应该有对中间件逻辑的支持。下面先来看看 net/http 能怎么去实现:

package main

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

// 错误处理中间件
func errorHandler(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                defer func() {
                        if err := recover(); err != nil {
                                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
                                log.Printf("Panic: %v", err)
                        }
                }()
                next.ServeHTTP(w, r)
        })
}

// 认证鉴权中间件
func authMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                // 模拟身份验证
                if r.Header.Get("Authorization") != "secret" {
                        http.Error(w, "Unauthorized", http.StatusUnauthorized)
                        return
                }
                next.ServeHTTP(w, r)
        })
}

// 处理业务逻辑
func helloHandler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World!")
}

// 另外
func anotherHandler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Another endpoint")
}

func main() {
        // 创建路由处理器
        router := http.NewServeMux()
        
        // 应用中间件, 注册处理器
        handler := errorHandler(authMiddleware(http.HandlerFunc(helloHandler)))
        router.Handle("/", handler)
        
        // 应用中间件, 注册另外一个请求的处理器
        another := errorHandler(authMiddleware(http.HandlerFunc(anotherHandler)))
        router.Handle("/another", another)
        
        // 启动服务器
        http.ListenAndServe(":8080", router)
}

在上述示例中,我们在net/http 中通过errorHandlerauthMiddleware 两个中间件实现了错误处理和鉴权功能。 接下来我们查看示例代码的第49行,可以发现代码通过装饰者模式,给原本的处理器增加了错误处理和鉴权操作功能。

这段代码的实现的优点,是通过装饰者模式,对多个处理函数进行组合,形成处理器链,实现了错误处理和认证鉴权功能。而不需要在每个处理函数handler 中去加上这部分逻辑,这使得代码具备更高的可读性和可维护性。

但是这里也存在着一个很明显的缺点,这个功能并不是框架给我们提供的,而是我们自己实现的。我们每新增一个处理函数handler, 都需要对这个handler 进行装饰,为其增加错误处理和鉴权操作,这在增加我们负担的同时,也容易出错。同时需求也是不断变化的,有可能部分请求只需要错误处理了,一部分请求只需要鉴权操作,一部分请求既需要错误处理也需要鉴权操作,基于这个代码结构,其会变得越来越难维护。

相比之下,Gin 框架提供了一种更灵活的方式来启用和禁用中间件逻辑,能针对某个路由组进行设置,而不需要对每个路由规则单独设置,下面展示下示例代码:

package main

import (
        "github.com/gin-gonic/gin"
)

func authMiddleware() gin.HandlerFunc {
        return func(c *gin.Context) {
                // 模拟身份验证
                if c.GetHeader("Authorization") != "secret" {
                        c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
                        return
                }
                c.Next()
        }
}

func main() {
        router := gin.Default()
        // 全局添加 Logger 和 Recovery 中间件
        // 创建一个路由组,该组中的所有路由都会应用 authMiddleware 中间件
        authenticated := router.Group("/")
        authenticated.Use(authMiddleware())
        {
                authenticated.GET("/hello", func(c *gin.Context) {
                        c.String(200, "Hello, World!")
                })

                authenticated.GET("/private", func(c *gin.Context) {
                        c.String(200, "Private data")
                })
        }

        // 不在路由组中,因此没有应用 authMiddleware 中间件
        router.GET("/welcome", func(c *gin.Context) {
                c.String(200, "Welcome!")
        })

        router.Run(":8080")
}

在上述示例中,我们通过router.Group("/") 创建了一个名为 authenticated 的路由组,然后使用 Use 方法,给该路由组启用 authMiddleware 中间件。在这路由组下所有的路由规则,都会自动执行authMiddleware 实现的鉴权操作。

相对于net/http 的优点,首先是不需要对每个handler 进行装饰,增加中间件逻辑,用户只需要专注于业务逻辑的开发即可,减轻了负担。

其次可维护性更高了,如果业务需要不再需要进行鉴权操作,gin 只需要删除掉Use 方法的调用,而net/http 则需要对所有handler的装饰操作进行处理,删除掉装饰者节点中的鉴权操作节点,工作量相对于gin 来说非常大,同时也容易出错。

最后,gin 在处理不同部分的请求需要使用不同中间件的场景下,更为灵活,实现起来也更为简单。比如 一部分请求需要鉴权操作,一部分请求需要错误里处理,还有一部分既需要错误处理,也需要鉴权操作。这种场景下,只需要通过gin 创建三个路由组router, 然后不同的路由组分别调用 Use 方法启用不同的中间件,即可实现需求了,这相对于net/http 更为灵活和可维护。

这也是为什么有net/http 的前提下,还出现了gin 框架的重要原因之一。

4. 数据绑定

在处理HTTP请求时,比较常见的功能,是将请求中的数据自动绑定到结构体当中。下面以一个表单数据为例,如果使用net/http,如何将数据绑定到结构体当中:

package main

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

type User struct {
        Name  string `json:"name"`
        Email string `json:"email"`
}

func handleFormSubmit(w http.ResponseWriter, r *http.Request) {
        var user User

        // 将表单数据绑定到 User 结构体
        user.Name = r.FormValue("name")
        user.Email = r.FormValue("email")

        // 处理用户数据
        fmt.Fprintf(w, "用户已创建:%s (%s)", user.Name, user.Email)
}

func main() {
        http.HandleFunc("/createUser", handleFormSubmit)
        http.ListenAndServe(":8080", nil)
}

我们需要调用FormValue 方法,一个一个得从表单中读取出数据,然后设置到结构体当中。而且在字段比较多的情况下,我们很有可能漏掉其中的某些字段,导致后续处理逻辑出现问题。而且每个字段都需要我们手动读取设置,也很影响我们的开发效率。

下面我们来看看Gin 是如何读取表单数据,将其设置到结构体当中的:

package main

import (
        "fmt"
        "github.com/gin-gonic/gin"
)

type User struct {
        Name  string `json:"name"`
        Email string `json:"email"`
}

func handleFormSubmit(c *gin.Context) {
        var user User

        // 将表单数据绑定到 User 结构体
        err := c.ShouldBind(&user)
        if err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": "无效的表单数据"})
                return
        }

        // 处理用户数据
        c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("用户已创建:%s (%s)", user.Name, user.Email)})
}

func main() {
        router := gin.Default()
        router.POST("/createUser", handleFormSubmit)
        router.Run(":8080")
}

看上面示例代码的第17行,可以看到直接调用ShouldBind 函数,便可以自动将表单的数据自动映射到结构体当中,不再需要一个一个字段读取,然后再单独设置到结构体当中。

相比于使用net/http, gin 框架在数据绑定方面更为方便,同时也不容易出错。gin 提供了各种 api , 能够将各种类型的数据映射到结构体当中,用户只需要调用对应的 api 即可。而net/http 则未提供相对应的操作,需要用户读取数据,然后手动设置到结构体当中。

5. 总结

在Go语言中, net/http 提供了基本的HTTP功能,但它的设计目标是简单和通用性,而不是提供高级特性和便利的开发体验。在处理HTTP请求和构建Web应用时,处理复杂的路由规则时,会显得力不从心;同时对于一些公共操作,比如日志记录,错误处理等,很难做到可插拔设计;想要将请求数据绑定到结构体中,net/http 也没有提供一些简易的操作,都是需要用户手动去实现的。

这就是为什么出现了像 Gin这样的第三方库,其是一个构建在 net/http 之上,旨在简化和加速Web应用程序的开发。

总的来说,Gin 可以帮助开发者更高效地构建Web应用程序,提供了更好的开发体验和更丰富的功能。当然,选择使用 net/http 还是 Gin 取决于项目的规模、需求和个人喜好。对于简单的小型项目,net/http 可能已经足够,但对于复杂的应用程序,Gin 可能会更适合。

posted @ 2023-08-07 08:08  菜鸟额  阅读(167)  评论(0编辑  收藏  举报