gin学习笔记(一)—— 了解gin
了解gin
Web编程基础
客户端和服务端
HTTP
客户端和服务器之间的请求响应一般都是使用 HTTP/HTTPS 协议,它规定了如何从网站服务器传输超文本到本地浏览器。
HTTP请求
当你在网页上点击一个链接、提交一个表单、或者进行一次搜索的时候,浏览器会发送一个 HTTP 请求给服务器。HTTP 请求有以下几种方式:
- GET请求 —— 获取资源
- POST请求 —— 发送、提交资源
- PUT请求 —— 更新资源
- DELETE请求 —— 删除资源
- HEAD请求 —— 获取报头,检测资源是否存在
- OPTIONS请求 —— 询问服务器所支持的HTTP请求方法
- PATCH请求 —— 局部更新资源
- CONNECT请求 —— 将连接改为管道方式的代理服务器
- TRACE请求 —— 回显服务器收到的请求,主要用于测试或诊断
关于HTTP请求还有很多内容,其中重要的是GET、POST、PUT、HEAD和DELETE,这里有个大概了解就行。
HTTP响应
HTTP 响应由四个部分组成, 分别是: 状态码、 响应报头、 空行、 响应正文
响应状态代码有三位数字组成, 第一个数字定义了响应的类别, 且有五种可能取值:
- 100~199: 表示服务器成功接收部分请求, 要求客户端继续提交其余请求才能完成整个处理过程。
- 200~299: 表示服务器成功接收请求并已完成整个处理过程。 常用 200(OK 请求成功)。
- 300~399: 为完成请求, 客户需进一步细化请求。 例如: 请求的资源已经移动一个新地址、 常用 302(所请求的页面已经临时转移至新的 url) 、 307 和 304(使用缓存资源)。
- 400~499: 客户端的请求有错误, 常用 404(服务器无法找到被请求的页面) 、 403(服务器拒绝访问, 权限不够)。
- 500~599: 服务器端出现错误, 常用 500(请求未完成。 服务器遇到不可预知的情况)。
gin简介
gin 是基于 httprouter 开发、使用 Go 语言编写的 Web 框架,实现动态路由,具有类似 martini 的 API,源码注释比较明确,具有快速灵活,容错方便等特点
特性
- 快速
基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。
-
支持中间件
传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。
-
Crash 处理
Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!
-
JSON 验证
Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。
-
路由组
更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。
-
错误管理
Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。
-
内置渲染
Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。
-
可扩展性
第一个gin程序
package main import ( "github.com/gin-gonic/gin" ) func main() { r := gin.Default() //创建默认的路由引擎 //配置路由,即配置url路径和回调函数 r.GET("/", func(c *gin.Context) { c.String(200, "这里是%v", "gin") }) r.GET("/hello", func(c *gin.Context) { c.String(200, "Hello world!") }) r.POST("/post", func(c *gin.Context) { //说明可以用post请求访问 /post c.String(200, "post请求的回调函数") }) r.PUT("/put", func(c *gin.Context) { //说明可以用post请求访问 /put c.String(200, "put请求的回调函数") }) r.DELETE("/delete", func(c *gin.Context) { //说明可以用post请求访问 /delete c.String(200, "delete请求的回调函数") }) //启动 http 服务,默认在 0.0.0.0:8080 启动 r.Run(":8000") //可以手动设置端口 }
运行程序后,在浏览器输入 127.0.0.1:8000 和 127.0.0.1:8000/hello
但是,当我们输入其他几个,如 127.0.0.1:8000/post 时,会出现 404 错误,这是因为通过浏览器的地址栏输入地址,所访问的URL都是get请求。我们可以使用 postman 进行其他请求:
gin路由
路由是指确定应用程序如何响应客户端对特定端点的请求,该特定端点是URI(或路径)和特定的HTTP请求方法(GET,POST等),根据URI上的路径,指引该条请求到对应的方法里去执行然后返回,中间可能会执行一些中间件。
每个路由可以具有一个或多个处理程序函数,这些函数在匹配该路由时执行。
静态路由和动态路由
-
静态路由: 框架/用户提前生成一个路由表,一般是map结构,key为URL上的path,value为代码执行点(处理函数),
- 优点:只需要读取map,没有任何开销,速度奇快
- 缺点:无法正则匹配路由,只能逐一对应,模糊匹配的场景无法使用
-
动态路由: 用户定义好路由匹配规则,框架匹配路由时,根据规则动态的去规划路由
- 优点:适应性强,解决了静态路由的缺点
- 缺点:相比静态路由有开销,具体视算法和路由匹配规则而定
gin框架就是动态路由。
gin路由基础使用
//普通路由 r.GET("/hello", func(c *gin.Context) {...}) r.POST("/hello", func(c *gin.Context) {...}) r.PUT("/put", func(c *gin.Context) {...}) //可以匹配所有请求方法的路由 r.any("/test", func(c *gin.Context) {...})
路由组
我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。
func main() { r := gin.Default() userGroup := r.Group("/user") { userGroup.GET("/index", func(c *gin.Context) {...}) userGroup.GET("/login", func(c *gin.Context) {...}) userGroup.POST("/login", func(c *gin.Context) {...}) } shopGroup := r.Group("/shop") { shopGroup.GET("/index", func(c *gin.Context) {...}) shopGroup.GET("/cart", func(c *gin.Context) {...}) shopGroup.POST("/checkout", func(c *gin.Context) {...}) } r.Run() }
通常我们将路由分组用在划分业务逻辑或划分API版本时。
重定向
HTTP重定向
r.GET("/test", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com/") })
路由重定向
路由重定向,使用HandleContext:
r.GET("/test", func(c *gin.Context) { // 指定重定向的URL c.Request.URL.Path = "/test2" r.HandleContext(c) }) r.GET("/test2", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"hello": "world"}) })
路由原理
Gin框架中的路由使用的是 httprouter 库,其基本原理就是构造一个路由地址的压缩前缀树。
前缀树的每一个节点代表一个字符串(前缀)。每一个节点会有多个子节点,通往不同子节点的路径上有着不同的字符。子节点代表的字符串是由节点本身的原始字符串,以及通往该子节点路径上所有的字符组成的。
举个例子:
r.GET("/", func(c *gin.Context) {...})
r.GET("/golang", func(c *gin.Context) {...}) r.GET("/golala", func(c *gin.Context) {...}) r.GET("/go", func(c *gin.Context) {...})
r.GET("/go/la", func(c *gin.Context) {...})
gin 的压缩前缀树变化如下:
gin 会为每种不同的请求方法构造相互独立的压缩前缀树,路由注册的过程是构造压缩前缀树的过程,路由匹配的过程就是查找压缩前缀树的过程。
中间件
中间件是一种特殊的处理函数,它在路由的处理函数执行前或执行后运行,常用于处理一些通用的任务,例如日志记录、错误处理、用户身份验证等。
Gin中的中间件必须是一个 gin.HandlerFunc 类型。也就是参数为 *gin.Context 的 functionValue (函数类型):
type HandlerFunc func(*Context)
中间件的时机
- 当一个HTTP请求到达时,Gin会按照注册的顺序调用每个中间件,对请求进行一些处理,例如检查请求头或请求体的内容、数据过滤等。
- 处理完请求后响应时,调用中间件处理,例如统一添加响应头、数据过滤等
- 中间件可以选择继续传递请求到下一个中间件,或者直接结束请求的处理并返回响应。
定义中间件
通过一个简单的例子来了解中间件:
//方法一
func SimpleLogger() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // 在请求被处理之前,记录一些信息 log.Printf("before request: %s\n", c.Request.URL.Path) // 调用下一个中间件或处理函数 c.Next() // 在请求被处理之后,记录一些信息 log.Printf("after request: %s, elapsed time: %v\n", c.Request.URL.Path, time.Since(t)) } }
当然,也可以这样:
//方法二 func Logger(c *gin.Context) { t := time.Now() // 在请求被处理之前,记录一些信息 log.Printf("before request: %s\n", c.Request.URL.Path) // 调用下一个中间件或处理函数 c.Next() // 在请求被处理之后,记录一些信息 log.Printf("after request: %s, elapsed time: %v\n", c.Request.URL.Path, time.Since(t)) }
注册中间件
可以调用 Use 方法注册中间件:
r.Use(SimpleLogger()) //对应上面方法一
r.Use(Logger) //对应方法二
全局注册
全局注册的中间件 会应用于所有路由: 会应用于所有在中间件后的路由:
func main() { r := gin.Default() r.GET("/", func(c *gin.Context) {...}) r.GET("/golang", func(c *gin.Context) {...}) //这两个都不会触发中间件
r.Use(SimpleLogger())
r.POST("/golang", func(c *gin.Context) {...}) //这些会触发 r.PUT("/go", func(c *gin.Context) {...}) r.Run() }
路由组注册
路由组注册的中间件会应用于路由组内所有路由:
//写法一 shopGroup := r.Group("/shop", SimpleLogger()) { shopGroup.GET("/index", func(c *gin.Context) {...}) ... } //写法二 shopGroup := r.Group("/shop") shopGroup.Use(SimpleLogger()) { shopGroup.GET("/index", func(c *gin.Context) {...}) ... }
单个路由注册
r.GET("/golang", SimpleLogger(), func(c *gin.Context) {...})
中间件工作流程
中间件的执行模型是洋葱模型:
像剥洋葱一样从最外层(最先注册)执行到最内层(最后注册)。
中间件之间通过 Context.Next() 来传递请求,也可以使用 Context.Abort() 直接结束请求
在了解Next和Abort之前,先看一下 Context 的部分数据结构:
//context.go type Context struct { ... handlers HandlersChain //函数切片 index int8 //函数切片的索引 ... } //gin.go type HandlersChain []HandlerFunc
Context.Next()
主要工作就是将 c.index++,然后 c.handlers[c.index](c),执行内层的函数,即剥下一层洋葱。
源码文件:gin/context.go line:171 func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } }
注意这里的 for 循环,它可以保证请求方法的执行,例如:中间件二没有添加 c.Next(),那么中间件一的 for 也能继续内层执行。是非常微妙的操作,这里还有一些细节在下方思考中描述。
Context.Abort()
主要作用就是将 index 设置为 63,然后结束请求,也就是直接不剥洋葱了。
源码文件:context.go line:47、188 const abortIndex int8 = math.MaxInt8 >> 1 func (c *Context) Abort() { c.index = abortIndex //63 }
为什么设置为 63 就能结束?这是因为 handlers 的长度必须小于 63,否则在注册时就会直接 panic.;所以 63 是超出长度的,不会执行任何函数然后结束。
中间件注意事项以及思考
注意事项
-
gin默认中间件
gin.Default() 默认使用了 Logger 和 Recovery 中间件,其中:
- Logger 中间件将日志写入 gin.DefaultWriter,即使配置了 GIN_MODE=release。
- Recovery 中间件会 recover 任何 panic。如果有 panic 的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用 gin.New() 新建一个没有任何默认中间件的路由。
-
gin中间件中使用 goroutine
当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()):
r.GET("/", func(c *gin.Context) { cCp := c.Copy() go func() { fmt.Println("Done! in path " + cCp.Request.URL.Path) }() c.String(200, "首页") })
思考
get请求后,以下程序会输出什么?为什么?
func m1(c *gin.Context) { log.Printf("m1-start") c.Next() log.Printf("m1-end") } func m2(c *gin.Context) { log.Printf("m2-start") //c.Next() log.Printf("m2-end") } func m3(c *gin.Context) { log.Printf("m3-start") //c.Next() log.Printf("m3-end") } func main() { r := gin.Default() r.Use(m1) r.Use(m2) r.Use(m3) r.GET("/golang", func(c *gin.Context) {
log.Printf("/golang") c.String(200, "Hello world!") })
r.Run() }

m1-start m2-start m2-end m3-start m3-end /golang m1-end
答:因为 c.Next() 有 for 循环保证执行,m2和m3都没有 c.Next(),因此没办法层层嵌套,都在 m1 的 for 循环中逐个执行了。
如果上题中的中间件都不使用c.Next(),会发生什么?为什么会这样?

m1-start m1-end m2-start m2-end m3-start m3-end /golang
答:这是因为如果有请求到达,那么 r.Run() 里会调用 c.Next()。其调用关系大致如下:
- 请求到达时的处理在 handleHTTPRequest 方法中(源码在 gin/gin.go中),里面有调用 c.Next();
- 而 handleHTTPRequest 是 ServerHTTP 方法(源码在 gin/gin.go中)调用的 ;
- 然后 ServerHTTP 是 Handler 接口要求的函数;
- 最后 r.Run() (源码在gin/gin.go中)有使用 Handler 接口变量。
小结
这一篇中,我们学习了路由和中间件的基础使用,大致了解其工作原理,压缩前缀树的图解和中间件的工作流程是本篇的重点。
中间件和路由是gin框架学习的非常重要的地方,这里只是简单描述,在之后还会涉及到。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现