Gin 渲染
HTML 渲染
(*gin.Engine).LoadHTMLGlob(pattern string):Gin 框架中按通配符规则批量加载 HTML 模板文件,完成模板引擎的初始化;加载后模板可通过文件名(含后缀)在c.HTML()中调用。适用于模板文件数量多、按目录 / 后缀统一归类(如所有模板放在templates/目录、均为.tmpl/.html后缀)的场景,简化多模板批量加载操作。(*gin.Engine).LoadHTMLFiles(files... string):Gin 框架中加载明确指定路径的 1 个或多个 HTML 模板文件,完成模板引擎的初始化;加载后模板可通过文件名(含后缀)在c.HTML()中调用。适用于模板文件数量少、文件路径明确固定的场景,精准加载所需模板,无多余文件匹配开销。
Go
func main() {
r := gin.Default() // 默认路由
// 解析 templates 当前目录及所有层级子目录的所有模板文件
r.LoadHTMLGlob("templates/**/*")
// 模板渲染部分:匹配 GET 方式的 /index 路径
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H {
"title": "Main Web",
})
})
// 启动服务器
r.Run(":8080")
}
该代码演示了简单的模板解析与渲染过程,r.GET() 是 Gin 框架提供的 GET 请求路由注册方法,用于将/index路径与对应的处理函数绑定,当客户端发起GET /index请求时,自动执行该处理函数并返回响应。下面是对模板渲染部分的解析:
r.GET("/index", 处理函数)
- 第一个参数代表请求路径,表示匹配的 HTTP GET 请求的 URL 路径,客户端访问
http://localhost:8080/index会触发该路由。 - 第二个参数为路由处理函数(Gin 规定的
gin.HandlerFunc类型),用于处理该路径的 GET 请求,完成业务逻辑并构建响应。处理函数的唯一入参必须是*gin.Context类型的指针(不可省略、不可修改类型),这是 Gin 框架的强制规范。
c.HTML(http.StatusOK, "index.tmpl", gin.H{...})
- 第一个参数为 HTTP 响应状态码(int 类型),
http.StatusOK等价于200,标识请求处理成功。 - 第二个参数代表模板文件名(字符串类型),必须与
r.LoadHTMLGlob("templates/*")加载的模板实际文件名完全一致(含后缀),Gin 会根据该名称匹配已加载的模板文件进行渲染。 - 第三个参数为模板渲染数据(
gin.H类型,本质是map[string]interface{}),键为模板中要使用的变量名,值为变量对应的实际数据。
自定义模板函数
Go 框架下的 template.Func(template.FuncMap){...} 在 Gin 框架中被替换成了 gin.Default().SetFuncMap(template.FuncMap){...}。(注意不能用 gin.H 代替 template.FuncMap,两者虽然底层类型完全一致,但框架对二者的使用场景和值的约束规则有严格区分)
Go
func main() {
r := gin.Default()
r.SetFuncMap(template.FuncMap {
// 避免转义
"safe": func(str string) template.HTML {
return template.HTML(str)
},
})
}
静态文件配置
gin.Static是 Gin 框架提供的静态文件/静态资源托管核心方法,用于将 URL 访问路径与服务器本地文件目录做映射,让客户端能通过 HTTP 请求直接访问服务器上的静态资源(如图片、CSS、JS、HTML等),底层基于 Go 原生net/http的文件服务实现,配置简单、性能高效。
func (engine *Engine) Static(relativePath , root string) IRoutes参数说明:
relativePath:URL 访问路径,如/static、/assetsroot:服务器本地文件目录
例如,对于以下项目目录结构(通用规范,静态资源统一放在 static 目录):
plaintext
my_project/
├── main.go // 主程序
└── static/ // 静态资源根目录
├── img/ // 图片目录
│ └── logo.png
├── css/ // 样式目录
│ └── main.css
└── js/ // 脚本目录
└── app.js
在 main.go 中配置 gin.Static:
Go
func main() {
r := gin.Default()
// URL 路径 /static 映射到本地 ./static 目录
r.Static("/static", "./static")
}
gin.StaticFS:是 Gin 框架提供的自定义文件系统的静态资源托管方法,在gin.Static基础上支持传入自定义的http.FileSystem接口实现,可灵活实现自定义文件访问规则(如隐藏指定文件、自定义文件读取逻辑、虚拟文件系统映射等),底层同样基于 Go 原生net/http实现,兼顾灵活性与性能,适用于需要对静态资源访问做个性化控制的场景。
func (engine *Engine) StaticFS(relativePath string, fs http.FileSystem) IRoutes参数说明:
relativePath:URL 访问路径,如/static、/assets,与gin.Static一致fs:实现了http.FileSystem接口的文件系统实例,Gin 常用gin.Dir(root string, listDirectory bool)快速创建(该方法是对原生文件系统的封装,支持控制是否允许目录列表访问)
Go
func main() {
r := gin.Default()
// URL 路径 /static 映射到本地 ./static 目录,禁止客户端访问目录列表
r.StaticFS("/static", gin.Dir("./static", false))
}
gin.StaticFile:是 Gin 框架提供的单文件静态资源托管方法,用于将单个具体的本地文件与指定的 URL 访问路径做一对一映射,适用于需要为单个静态文件配置独立访问路径、或隐藏文件实际存储路径的场景(如 /favicon.ico、/robots.txt 这类特殊单文件),底层基于 Go 原生http.ServeFile实现,轻量高效。
func (engine *Engine) StaticFile(relativePath string, filePath string) IRoutes参数说明:
relativePath:单个文件的专属 URL 访问路径,如/favicon.ico、/logofilePath:服务器本地单个文件的绝对路径或相对路径,必须指向具体文件(而非目录),路径需准确包含文件名和后缀
Go
func main() {
r := gin.Default()
// 配置场景1:为网站/favicon.ico配置映射(浏览器会自动请求该路径,需单独配置)
r.StaticFile("/favicon.ico", "./static/img/logo.png")
// 配置场景2:为单个图片配置简洁访问路径,隐藏实际存储目录
r.StaticFile("/logo", "./static/img/logo.png")
}
模板继承
Gin 原生模板引擎仅支持子模板调用,不直接支持模板继承,而 github.com/gin-contrib/multitemplate 库是 Gin 生态的官方推荐扩展,专门解决这一问题 ------ 通过自定义模板渲染器,实现模板继承、块重写、公共模板复用的核心能力,让 Gin 模板开发更符合主流 Web 开发习惯。模板继承要求按 基础模板目录 + 业务模板目录 划分,规范目录结构通常如下:
plaintext
my_project/
├── main.go // 主程序
└── templates/ // 模板根目录
├── layouts/ // 基础模板(布局模板)目录
│ └── base.tmpl // 核心基础模板(母版,含公共布局)
├── user/ // 用户模块业务模板目录
│ └── info.tmpl // 用户信息页(子模板,继承base.tmpl)
└── index.tmpl // 首页(子模板,继承base.tmpl)
注意:基础模板需用
{``{define "layouts/base}}定义唯一标识名,通常建议与文件路径一致,避免冲突,所有块定义在该标识内
在子模板编写好后,核心是通过 github.com/gin-contrib/multitemplate 库自定义 Gin 模板渲染器,替代原生模板引擎。
核心库函数及其作用
该库的核心思想是手动构建一个模板映射(Map),将一个逻辑名称与一组物理文件绑定。
multitemplate.NewRenderer() multitemplate.Renderer:初始化一个新的渲染器对象,该对象是所有模板注册、解析、渲染的核心载体,后续所有模板操作都基于此对象。(*multitemplate.Renderer).AddFromFiles(name, files... string):用于将多个模板文件(基础模板与业务模板),实现模板继承的关键函数。第一个参数是模板标识名,建议用业务模板的文件名;对于第二个可变参数,用于接收模板文件的路径切片,传参时必须将模板路径放在最前面,业务模板路径放在最后,且每个业务模板都需要单独调用该方法注册。
模板继承示例
Go
// 加载基础模板与业务模板,构建支持继承的渲染器
// 入参为模板根目录如 "./templates"
func loadTemplates(templatesDir string) multitemplate.Renderer {
// 初始化库的渲染器对象
r := multitemplate.NewRenderer()
// 加载 layouts 下的所有基础模板
// filepath.Glob:匹配指定路径下的模板文件,返回文件路径切片
layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
// 加载业务模板
includes1, err := filepath.Glob(templatesDir + "/user/*.tmpl") // user 模块业务模板
includes2, err := filepath.Glob(templatesDir + "/*.tmpl") // 根目录业务模块
includes := append(includes1, includes2...) // 合并所有业务模板路径
// 用基础模板与每个业务模板进行拼接后注册
for _, include := range includes {
// 复制基础模板路径切片
layoutCopy := make([] string, len(layouts))
copy(layoutCopy, layouts)
files := append(layoutCopy, include) // 组合模板文件
// filepath.Base(include) 获取业务模板的文件名作为模板标识
r.AddFromFiles(filepath.Base(include), files...)
}
return r
}
在主函数中,将上述自定义加载函数返回的multitemplate.Renderer 渲染器,赋值给 Gin 引擎的 HTMLRender 属性,Gin 会放弃原生模板解析逻辑,改用 multitemplate 库的渲染器处理所有 c.HTML 模板渲染请求,实现模板继承。
Go
func main() {
r := gin.Default()
// 将自定义渲染器赋值给 Gin 的 HTMLRender
r.HTMLRender = loadTemplates("./templates")
// 注册路由,关联渲染函数
r.GET("/index", func(c *gin.Context) {
c.HTML(gin.StatusOK, "index.tmpl", gin.H{
"title": "首页",
})
})
r.GET("/user/info", func(c *gin.Context) {
c.HTML(gin.StatusOK, "info.tmpl", gin.H{
"user": gin.H{"name": "张三", "age": 25,},
})
})
// 启动服务
r.Run(":8080")
}
当执行 c.HTML(StatusOK, "index.tmpl", data) 时,渲染器会同时加载基础模板和该业务模板,index.tmpl 里的 {``{define "content"}} 块会自动覆盖 base.tmpl 里的 {``{block "content" .}}{``{end}} 占位符。
注意:使用该库后,禁止调用 Gin 原生的 r.LoadHTMLGlob()/r.LoadHTMLFiles(),否则会与 multitemplate 渲染器冲突,导致模板继承失效。
JSON 渲染
在 Gin 框架中,c.JSON() 是开发 RESTful API 最常用的方法。它能自动将 Go 语言中的数据结构(如 Map、结构体)序列化为 JSON 格式的字符串,并将响应头的 Content-Type 自动设置为 application/json; charset=utf-8。
Go
func main() {
r := gin.Default()
// 方式一:使用 gin.H 快速拼接 JSON
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello world!"})
})
// 方式二:使用结构体渲染
r.GET("/moreJSON", func(c *gin.Context) {
// 定义一个匿名结构体
var msg struct {
Name string `json:"user"` // 使用 json 标签定制生成的键名
Message string
Age int
}
msg.Name = "张三"
msg.Message = "Hello world!"
msg.Age = 18
c.JSON(http.StatusOK, msg)
})
r.Run(":8080")
}
通过 r.GET() 注册路由后,处理函数利用 c.JSON() 返回格式化的数据,该代码演示了两种主流的 JSON 渲染方式:
- 使用
gin.H拼接:不需要预先定义类型,随写随用。适用于返回简单的错误消息、状态通知或结构不固定的数据。 - 使用结构体渲染:在结构体字段后方标注的
json:"user"称为 JSON 标签。其作用为控制生成的 JSON 键名,在示例中,虽然 Go 字段名是Name(大写开头以保证可导出),但通过标签,最终返回给客户端的 JSON 键名为小写的"user"(如果没有标签,JSON 的键名将直接使用 Go 的字段名)。在处理大量复杂数据时,结构体序列化的效率通常高于 Map,且减少了手写 Map 时键名拼错的风险,便于文档化。
获取参数
获取 querystring 参数
querystring 指的是 URL 中 ? 后面携带的参数,例如 https://www.google.com/search?q=迷迭香 就是谷歌搜索 迷迭香 时的 URL。在 Gin 框架中提供了多种从 gin.Context 中提取这些参数的方法:
- 获取基本字符串参数
c.Query(key string) string:获取指定 key 的值,如果 key 不存在,返回空字符串。
Go
// URL: /path?name=rosemary
name := c.Query("name") // rosemary
address := c.Query("addr") // 不存在则为空
-
c.DefaultQuery(key, defaultValue string) string:获取指定 key 的值,如果 key 不存在,则返回提供的默认值。 -
c.GetQuery(key string) (string, bool):获取值并判断该 key 是否真的存在。多返回的布尔值在需要区分参数为空和参数未传时非常有用。
Go
if name, ok := c.GetQuery("name"); ok {
// 参数存在
} else {
// 参数不存在
}
- 获取数组/切片参数(多个同名 Key):当 URL 形式是
?tags=go&tags=gin&tags=web时,需要获取数组。
c.QueryArray(key string) []string:获取同名 key 的所有值,返回一个字符串切片。
- 获取 Map 参数:当 URL 形式为
?user[id]=1&user[name]=rosemary时。
c.QueryMap(key string) map[string]string:获取 key 后面带中括号的映射关系。
Go
// URL: /path?user[id]=1&user[name]=admin
user := c.QueryMap("user") // map[id:1 name:admin]
c.GetQueryMap(key string) (map[string]string, bool):同上,带存在性检查。
- 结构体绑定:如果有很多参数,调用
c.Query过于繁琐。可以使用ShouldBindQuery将参数直接映射到结构体。
c.ShouldBindQuery(obj any) error:定义结构体并使用form标签。
Go
type Filter struct {
Name string `form:"name"`
Age int `form:"age"`
Page int `form:"page" binding:"required"` // 还可以做简单校验
}
func search(c *gin.Context) {
var filter Filter
// 根据 form 标签自动匹配 URL 参数
if err := c.ShouldBindQuery(&filter); err == nil {
fmt.Printf("%#v\n", filter)
}
}
获取 form 参数
在 Gin 框架中,获取 Form 表单参数通常用于处理 HTTP POST 或 PUT 请求,且请求的 Content-Type 为 application/x-www-form-urlencoded 或 multipart/form-data(如文件上传)。
- 获取基本字符串参数
c.PostForm(key string) string:从表单中获取指定 key 的第一个值。如果 key 不存在,返回空字符串。
Go
// 假设前端发送了 username = rosemary, password = 123456
username := c.PostForm("username") // "rosemary"
password := c.PostForm("password") // "123456"
c.DefaultPostForm(key, defaultValue string) string:获取表单参数,如果 key 不存在,返回指定的默认值。c.GetPostForm(key string) (string, bool):获取值并返回该 key 是否存在的布尔值。
- 获取数组与 Map 参数
c.PostFormArray(key string) []string:获取表单中同名的多个 key 的值(常见于多选框)。c.PostFormMap(key string) map[string]string:获取表单中符合 map 格式的参数。
- 文件上传专用函数:Form 表单常用于上传文件,Gin 提供了专门的函数
c.FormFile(name string) (*multipart.FileHeader, error):获取上传的单个文件,传入的参数name是前端表单中文件上传字段的name属性值。
Go
file, err := c.FormFile("avatar")
if err == nil {
// 保存文件到本地
c.SaveUploadedFile(file, "./uploads/"+file.Filename)
}
c.MultipartForm() (*multipart.Form, error):获取整个multipart表单,包含多个文件和多个参数。
Go
form, err := c.MultipartForm()
// form.File 是一个 map[string][]*multipart.FileHeader,用来存储请求中所有上传的文件
files := form.File["photos"] // 获取名为 photos 的多个文件
- 结构体绑定:在实际开发中,直接使用
PostForm比较繁琐。使用ShouldBind可以一键将表单数据映射到结构体。
c.ShouldBind(obj any) error:根据请求的Content-Type自动判断(如果是表单则找form标签)。
Go
type LoginForm struct {
User string `form:"username" binding:"required"`
Password string `form:"password"`
}
func loginHandler(c *gin.Context) {
var form LoginForm
// 自动解析表单数据并赋值给 form 变量
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
fmt.Println(form.User, form.Password)
}
获取 path 参数
在 Gin 框架中,Path 参数(路径参数)是指 URL 路径中作为动态路由的一部分,通常在定义路由时使用 :column(命名参数)或 *column(通配符参数)来指定。
- 获取单个路径参数
c.Param(key string) string:获取路由规则中对应的动态参数值。直接返回字符串,如果参数不存在,返回空字符串。
Go
// 命名参数:只匹配路径中两个 `/` 之间的部分
// 路由定义:r.GET("user/:id", ...)
// 用户访问:/user/123
id := c.Param("id") // id 的值是 "123"
// 通配符参数:匹配从该位置开始的所有路径
// 路由定义: r.GET("/user/:id/*action", ...)
// 用户访问: /user/123/send/message/mail
id := c.Param("id") // "123"
action := c.Param("action") // "/send/message/mail"(注意:包括开头斜杠)
- 结构体绑定路径参数 (推荐用于多参数):当需要获取多个路径参数并进行类型转换(如字符串转 int)时,手动调用
c.Param会很繁琐,此时应使用 URI 绑定。
c.ShouldBindUri(obj any) error:将路径参数自动绑定到结构体字段上,需要在结构体上使用uri标签。它可以自动处理类型转换。
Go
type UserInfo struct {
ID int `uri:"id" binding:"required"` // 自动转为 int
Name string `uri:"name" binding:"required"`
}
// 路由定义: r.GET("/user/:id/:name", ...)
// 用户访问: /user/7/rosemary
func getPathFunc(c *gin.Context) {
var user UserInfo
if err := c.ShouldBindUri(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H {
"msg": err.Error(),
})
} else {
c.JSON(http.StatusOK, gin.H {
"ID": user.ID,
"Name": user.Name,
})
}
}
- 获取所有路径参数
c.Params:c.Params实际上是一个Params切片([]Param),存储了当前请求中所有的键值对,通常用于需要遍历所有路径参数的底层场景。
Go
// 路由定义:/user/:id/:name
for _, p := range c.Params {
fmt.Printf("Key: %s, Value: %s\n", p.Key, p.Value)
}
获取 JSON 参数
在 Gin 框架中,获取 JSON 参数并不像获取 URL 参数那样通过单一的 key-value 函数(如 Query 或 PostForm),而是主要依赖于模型绑定(Binding)机制。在使用以下函数前,必须先定义一个结构体,并使用 json:"..." 标签来指定 JSON 键名,可选 binding:"required" 标签进行参数校验。
Go
type UserInfo struct {
// json:"name" 表示解析 JSON 中的 "name" 字段
// binding:"required" 表示如果 JSON 中没传这个字段,则绑定失败
Name string `json:"name" binding:"required"`
Age int `json:"age"`
}
c.ShouldBindJSON():开发中最常用的函数。它会尝试将请求体中的 JSON 数据解析到结构体中。如果解析失败,它会返回错误,由开发者决定如何处理错误(例如返回自定义的 JSON 错误提示)。
Go
func HandleJSON(c *gin.Context) {
var user UserInfo
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error(),})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Hello" + user.Name,})
}
c.BindJSON():这个函数内部调用了ShouldBindJSON。区别在于,如果绑定失败,它会自动在响应头中写入400状态码,并中断请求。c.ShouldBind():这是一个通用绑定函数。它会根据请求头的Content-Type自动判断是 JSON、XML 还是 Form 表单。- 解析到 Map:如果你不确定 JSON 的具体结构,或者不想定义结构体,可以将其解析到
map[string]interface{}中。
Go
func HandleMap(c *gin.Context) {
var data map[string]interface{}
if err := c.ShouldBindJSON(&data); err == nil {
// 通过 key 获取数据
fmt.Println(data["name"])
}
}
重定向
在 Gin 框架中,重定向分为 HTTP 重定向 (外部重定向)和 路由重定向(内部转发)。理解它们的区别对于处理登录跳转、URL 规范化以及架构解耦至关重要。
- HTTP 重定向:是最常用的重定向方式。服务器通知浏览器,其要找的内容在另一个地址,让其重新发起请求。特点是浏览器的地址栏会改变,产生了两次 HTTP 请求。
http.StatusMovedPermanently(301):永久重定向,搜索引擎会将旧地址的权重转移到新地址(SEO 友好)。http.StatusFound(302):临时重定向。最常用,表示本次跳转仅限当前,后续请求仍访问原地址。http.StatusTemporaryRedirect(307):使得POST请求重定向后依然保持POST方法(而不是默认变成GET)
Go
// 核心函数:c.Redirect(Statuscode int, location string)
// A. 重定向到外部网站
r.GET("/google", func(c *gin.Context) {
// 强制跳转到外部链接
c.Redirect(http.StatusMovedPermanently, "https://www.google.com")
})
// B. 站内路径跳转(例如登录后跳转)
r.GET("/login_success", func(c *gin.Context) {
// 跳转到本站的 /index 路由
c.Redirect(http.StatusFound, "/index")
})
// C. 浏览器收到 307 后,会携带原有的 POST 数据重新请求 /new_api
r.POST("/old_api", func(c *gin.Context) {
// 浏览器收到 307 后,会携带原有的 POST 数据重新请求 /new_api
c.Redirect(http.StatusTemporaryRedirect, "/new_api")
})
- 路由重定向 (Route Redirection / Internal Forwarding):这种方式是服务器内部的操作。当请求到达 A 路由时,服务器内部直接调用 B 路由的处理逻辑。特点是浏览器的地址栏不会改变;只产生了一次 HTTP 请求。
- 修改路径并重新执行 (HandleContext):在 Gin 中,如果要实现内部重定向,通常需要修改请求的 Path,然后调用
r.HandleContext(c)。
Go
func main() {
r := gin.Default()
r.GET("/test", func(c *gin.Context) {
// 修改请求的路径
c.Request.URL.Path = "/newtest"
// 重新执行路由分发逻辑
r.HandleContext(c)
})
r.GET("/newtest", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"hello": "world (from newtest)"})
})
}
Gin 路由
在 Gin 框架中,路由(Routing)是核心组件。Gin 的路由逻辑虽然是基于自己实现的,但其核心算法深受 httprouter 库的影响(使用前缀树算法)。
普通路由
普通路由直接注册在 gin.Default() 返回的 *gin.Engine 实例上。
- 标准 HTTP 动词函数:这些函数用于匹配特定的请求方法。
Go
// 用法:r.Method(path, handlers)
// 常用函数:GET(), POST(), PUT(), DELETE(), PATCH(), OPTIONS(), HEAD()
r := gin.Default()
// 静态路径
r.GET("/hello", func(c *gin.Context) { c.String(200, "Hello") })
// POST请求
r.POST("/login", loginHandler)
- 匹配所有动词:无论客户端使用 GET、POST 还是其他方法访问该路径,都会触发。
Go
// 核心函数:Any()
r.Any("/all", func(c *gin.Context) {
c.String(StatusOK, "Method is %s", c.Request.Method)
})
- 特殊路由:
NoRoute()和NoMethod():r.NoRoute()当请求的 URL 找不到匹配的路由时触发(自定义 404);r.NoMethod()当路径匹配但 HTTP 方法不匹配时触发(如路由定义了 GET,但收到了 POST)。
路由组(Route Group)常用函数
路由组用于逻辑分类和简化代码,例如区分 API 版本或模块,路由组内能继续定义新的路由组,称为 嵌套路由组。
r.Group(relativePath string, middlewares ...gin.HandlerFunc):以指定的相对路径前缀,创建一个路由分组,分组内所有路由都会自动拼接该前缀,实现路由归类(在 Go 语言及 Gin 框架的行业开发规范中,习惯性用一对{}包裹同组的路由代码)。
Go
func main() {
r := gin.Default()
// 创建路由分组:前缀为/user
userGroup := r.Group("/user")
{
// 为分组注册路由(自动拼接前缀:/user/info)
userGroup.GET("/info", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "获取用户信息", "code": 200})
})
// 自动拼接前缀:/user/update
userGroup.POST("/update", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "更新用户信息", "code": 200})
})
}
// 非分组路由:不会执行userGroupLogMiddleware中间件
r.GET("/index", func(c *gin.Context) {
c.String(http.StatusOK, "首页(无分组中间件)")
})
r.Run(":8080")
}
Gin 中间件
中间件是 Gin 框架中一类特殊的 gin.HandlerFunc 函数,它的核心作用是在路由处理函数执行的前后,插入通用的公共逻辑,实现代码复用和请求流程的统一管控。简单来说,中间件是请求的拦截器 / 处理器,请求到达路由函数前先经过中间件,路由函数执行完后还能再经过中间件,全程不破坏原有路由的业务逻辑。中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
中间件基础函数
- 中间件注册函数
r.Use(middlewares ...gin.HandlerFunc):注册的中间件将作用于所有的请求(包括所有路由组和 404 页面),支持传入一个或多个中间件,按传入顺序执行前置逻辑。
Go
r := gin.New()
r.Use(gin.Logger(), gin.Recovery))
group.Use (middlewares ...gin.HandlerFunc):为当前分组下的所有路由(包括子分组) 注册中间件,仅对分组内路由生效,不影响其他路由/分组。
Go
v1 := r.Group("/v1")
v1.Use(AuthMiddleware()) // 只有 v1 开头的路径会进行身份校验
{
v1.GET("/profile", profileHandler)
}
- 中间件流控函数:在中间件函数内部,通过以下三个函数控制请求的流向。
c.Next():挂起当前的中间件,先去执行后续的中间件或业务逻辑(Handler)。等到后续逻辑执行完毕后,再回到当前中间件继续执行c.Next()后面的代码(通常运用于统计响应时间、统一日志打印等)。
Go
func MyMiddleware(c *gin.Context) {
start := time.Now()
c.Next() // 调用后续的 Handler
// 以下代码在 Handler 执行完后运行
cost := time.Since(start)
fmt.Printf("耗时:%v\n", cost)
}
c.Abort():终止后续所有的中间件和 Handler 的执行,当前中间件内c.Abort()之后的代码依然会执行完,但不会再进入下一个环节(通常运用于权限校验失败、黑名单拦截)。
Go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "未登录",})
c.Abort() // 拦截,后续的 Handler 不会再执行
return
}
c.Next()
}
}
c.AbortWithStatusJSON(Statuscode int, obj any):c.Abort()的加强版,在终止执行的同时,直接向客户端返回指定的 JSON 数据和状态码,替代c.Abort()与c.JSON()组合的写法。
Go
// 实际开发中推荐封装统一的错误响应结构体,让接口返回格式更规范
// 定义全局统一的 JSON 响应结构体
type Response struct {
Code int `json:"code"` // 业务码(非HTTP状态码)
Msg string `json:"msg"` // 提示信息
Data interface{} `json:"data"` // 响应数据(nil/对象/数组)
}
// 登录校验中间件
func loginCheck(c *gin.Context) {
token := c.GetHeader("token")
if token == "" {
// 返回自定义结构体,自动序列化为 JSON
c.AbortWithStatusJSON(http.StatusUnauthorized, Response{
Code: 10001,
Msg: "未授权,token为空",
Data: nil,
})
}
c.Next()
}
- 中间件间的数据传递:由于中间件和 Handler 共享同一个
*gin.Context,可以使用键值对传递数据。
c.Set(key string, value any):在中间件里解析出用户信息后,存入上下文,方便后面的业务逻辑直接取出。c.Get(key string):获取中间件存入的值。
Go
// 在 Handler 中获取中间件存入的值
if val, exists := c.Get("userId"); exists {
userId := val.(int) // 注意需要类型断言
}
常用内置中间件
gin.Logger():全局请求日志中间件,记录所有请求的核心信息(请求方法、路径、状态码、耗时、请求体大小等),输出到控制台(默认),方便开发调试和生产环境日志排查。gin.LoggerWithWriter(wr io.Writer):允许自定义日志输出到哪里(可以是文件、网络套接字、内存缓冲区等),只要该对象实现了io.Writer接口(默认情况下,gin.DefaultWriter = os.Stdout,而gin.Logger()等同于gin.LoggerWithWriter(gin.DefaultWriter)。
Go
func main() {
r := gin.New()
// // 创建日志文件,追加写入
f, _ := os.Create("gin.log")
// 将日志输出到文件(而非控制台)
r.Use(gin.LoggerWithWriter(f))
r.Use(gin.Recovery())
r.GET("/index", func(c *gin.Context) {
c.String(200, "首页")
})
r.Run(":8080")
}
gin.Recovery():全局异常恢复中间件,捕获请求处理过程中的 panic 异常,防止程序崩溃退出,同时返回 HTTP 500 状态码给客户端,是生产环境必须注册的中间件(gin.Default()已默认包含)。gin.RecoveryWithWriter(wr io.Writer):允许指定一个io.Writer接口对象(比如一个文件、缓冲区或网络流),将异常信息写入该目标(gin.Recovery()通常等同于RecoveryWithWriter(DefaultErrorWriter),而DefaultErrorWriter默认是os.Stderr。
Go
func main() {
// 创建或打开一个日志文件
f, _ := os.Create("gin_error.log")
// 使用 RecoveryWithWriter 将错误日志重定向到文件
r := gin.New()
r.Use(gin.RecoveryWithWriter(f))
r.GET("/panic", func(c *gin.Context) {
// 模拟一个崩溃
panic("服务器意外宕机了!")
})
r.Run(":8080")
}
gin.BasicAuth(accounts Accounts):基础 HTTP 认证中间件,实现简单的用户名与密码校验机制,适用于小型服务、测试接口的轻量权限控制,无需复杂的 Token / 会话认证体系;入参gin.Accounts是键值对映射(key 为用户名,value 为密码),认证失败时自动返回 HTTP 401 状态码,浏览器会弹出原生认证弹窗。
Go
func main() {
r := gin.New()
authAccounts := gin.Accounts {
"admin": "admin123",
"guest": "guest456",
}
// 为/admin分组注册基础认证中间件,分组内所有路由需认证才能访问
adminGroup := r.Group("/admin", gin.BasicAuth(authAccounts))
{
adminGroup.GET("/dashboard", func(c *gin.Context) {
// c.MustGet(gin.AuthUserKey) 获取当前通过认证的用户名
username := c.MustGet(gin.AuthUserKey).(string)
c.JSON(200, gin.H{
"user": username,
"msg": "认证成功",
})
})
}
r.Run(":8080")
}
gin.MaxMultipartMemory():文件上传大小限制中间件,专门限制multipart/form-data类型请求的最大数据体积(包含上传文件与表单数据),防止客户端上传超大文件导致服务器磁盘 / 内存溢出,是文件上传接口的必备中间件;入参为字节数,超出限制时自动拦截并返回错误。
Go
func main() {
r := gin.New()
// 全局限制文件上传最大体积为8MB,所有上传请求均受此限制
r.Use(gin.MaxMultipartMemory(8 << 20))
r.POST("/upload/avatar", func(c *gin.Context) {
file, err := c.FormFile("avatar")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.SaveUploadedFile(file, "./uploads/" + file.Filename)
c.JSON(200, gin.H{"msg": "文件上传成功", "filename": file.Filename,})
})
}
中间件注意事项
- 中间件中的协程:如果在中间件中需要启动新的 Goroutine(协程)处理任务(如异步写日志、发邮件),错误做法是直接在协程里使用原始的
c *gin.Context;必须使用c.Copy()获取一个只读副本,因为原始 Context 会在请求结束后被销毁或回收。
Go
func AsyncTaskMiddleware(c *gin.Context) {
cCp := c.Copy() // 必须拷贝
go func() {
time.Sleep(3 * time.Second)
fmt.Println("异步处理完成,路径:" + cCp.Request.URL.Path)
}()
}
-
洋葱模型执行顺序
假设有一条路由注册了两个中间件 M1, M2 和一个处理函数 H:
r.GET("/", M1, M2, H)。执行顺序为:进入 M1(执行c.Next()之前的代码);进入 M2(执行c.Next()之前的代码); 进入 H(业务逻辑执行); 回到 M2(执行c.Next()之后的代码);回到 M1(执行c.Next()之后的代码)。如果 M1 中调用了c.Abort(),那么只会执行 M1 中c.Abort()及其后的代码,M2 和 H 永远不会被执行。 -
gin.Default()默认使用了gin.Logger()和gin.Recovery()中间件,即gin.Default()等价于gin.New()加上r.Use(gin.Logger(), gin.Recovery())。