有了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 可能会更适合。

本文由博客一文多发平台 OpenWrite 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 最近将gin的源码看了一遍, 会用几篇文章将gin的流程及流程做一个梳理, 下面进入正题. gin框架预览 上图大...
    HHFCodeRv阅读 3,375评论 2 11
  • gin,beego等底层都用的是net/http模块,上篇文章中对一个基本的http请求作了分析,这篇文章就gin...
    zzzyyy111阅读 1,381评论 0 0
  • gin框架总结 一 gin框架初识 1.1 helloworld gin框架中的路由是基于httprouter[h...
    voidFan阅读 5,343评论 0 7
  • 一、gin简介 Gin 是一个用 Go (Golang) 编写的 HTTP web 框架。 它是一个类似于 mar...
    Every_dawn阅读 2,411评论 1 4
  • Gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了htt...
    雪上霜阅读 1,583评论 0 1