Gin 框架

Gin 渲染

HTML 渲染

  1. (*gin.Engine).LoadHTMLGlob(pattern string):Gin 框架中按通配符规则批量加载 HTML 模板文件,完成模板引擎的初始化;加载后模板可通过文件名(含后缀)在 c.HTML() 中调用。适用于模板文件数量多、按目录 / 后缀统一归类(如所有模板放在 templates/ 目录、均为 .tmpl/.html 后缀)的场景,简化多模板批量加载操作。
  2. (*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请求时,自动执行该处理函数并返回响应。下面是对模板渲染部分的解析:

  1. r.GET("/index", 处理函数)
  • 第一个参数代表请求路径,表示匹配的 HTTP GET 请求的 URL 路径,客户端访问http://localhost:8080/index会触发该路由。
  • 第二个参数为路由处理函数(Gin 规定的gin.HandlerFunc类型),用于处理该路径的 GET 请求,完成业务逻辑并构建响应。处理函数的唯一入参必须是*gin.Context类型的指针(不可省略、不可修改类型),这是 Gin 框架的强制规范。
  1. 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)
		},
	})
}

静态文件配置

  1. gin.Static 是 Gin 框架提供的静态文件/静态资源托管核心方法,用于将 URL 访问路径与服务器本地文件目录做映射,让客户端能通过 HTTP 请求直接访问服务器上的静态资源(如图片、CSS、JS、HTML等),底层基于 Go 原生 net/http 的文件服务实现,配置简单、性能高效。
    func (engine *Engine) Static(relativePath , root string) IRoutes 参数说明:
  • relativePath:URL 访问路径,如 /static/assets
  • root:服务器本地文件目录

例如,对于以下项目目录结构(通用规范,静态资源统一放在 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")
}
  1. 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))
}
  1. gin.StaticFile:是 Gin 框架提供的单文件静态资源托管方法,用于将单个具体的本地文件与指定的 URL 访问路径做一对一映射,适用于需要为单个静态文件配置独立访问路径、或隐藏文件实际存储路径的场景(如 /favicon.ico、/robots.txt 这类特殊单文件),底层基于 Go 原生 http.ServeFile 实现,轻量高效。
    func (engine *Engine) StaticFile(relativePath string, filePath string) IRoutes 参数说明:
  • relativePath:单个文件的专属 URL 访问路径,如 /favicon.ico/logo
  • filePath:服务器本地单个文件的绝对路径或相对路径,必须指向具体文件(而非目录),路径需准确包含文件名和后缀
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),将一个逻辑名称与一组物理文件绑定。

  1. multitemplate.NewRenderer() multitemplate.Renderer:初始化一个新的渲染器对象,该对象是所有模板注册、解析、渲染的核心载体,后续所有模板操作都基于此对象。
  2. (*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 渲染方式:

  1. 使用 gin.H 拼接:不需要预先定义类型,随写随用。适用于返回简单的错误消息、状态通知或结构不固定的数据。
  2. 使用结构体渲染:在结构体字段后方标注的 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 中提取这些参数的方法:

  1. 获取基本字符串参数
  • 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 {
    // 参数不存在
}
  1. 获取数组/切片参数(多个同名 Key):当 URL 形式是 ?tags=go&tags=gin&tags=web 时,需要获取数组。
  • c.QueryArray(key string) []string:获取同名 key 的所有值,返回一个字符串切片。
  1. 获取 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):同上,带存在性检查。
  1. 结构体绑定:如果有很多参数,调用 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 POSTPUT 请求,且请求的 Content-Typeapplication/x-www-form-urlencodedmultipart/form-data(如文件上传)。

  1. 获取基本字符串参数
  • 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 是否存在的布尔值。
  1. 获取数组与 Map 参数
  • c.PostFormArray(key string) []string:获取表单中同名的多个 key 的值(常见于多选框)。
  • c.PostFormMap(key string) map[string]string:获取表单中符合 map 格式的参数。
  1. 文件上传专用函数: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 的多个文件
  1. 结构体绑定:在实际开发中,直接使用 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(通配符参数)来指定。

  1. 获取单个路径参数
  • 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"(注意:包括开头斜杠)
  1. 结构体绑定路径参数 (推荐用于多参数):当需要获取多个路径参数并进行类型转换(如字符串转 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,
		})
	}
}
  1. 获取所有路径参数
  • c.Paramsc.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 函数(如 QueryPostForm),而是主要依赖于模型绑定(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"`
}
  1. 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,})
}
  1. c.BindJSON():这个函数内部调用了 ShouldBindJSON。区别在于,如果绑定失败,它会自动在响应头中写入 400 状态码,并中断请求。
  2. c.ShouldBind():这是一个通用绑定函数。它会根据请求头的 Content-Type 自动判断是 JSON、XML 还是 Form 表单。
  3. 解析到 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 规范化以及架构解耦至关重要。

  1. 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")
})
  1. 路由重定向 (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 函数,它的核心作用是在路由处理函数执行的前后,插入通用的公共逻辑,实现代码复用和请求流程的统一管控。简单来说,中间件是请求的拦截器 / 处理器,请求到达路由函数前先经过中间件,路由函数执行完后还能再经过中间件,全程不破坏原有路由的业务逻辑。中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

中间件基础函数

  1. 中间件注册函数
  • 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)
}
  1. 中间件流控函数:在中间件函数内部,通过以下三个函数控制请求的流向。
  • 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()
}
  1. 中间件间的数据传递:由于中间件和 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) // 注意需要类型断言
}

常用内置中间件

  1. gin.Logger() :全局请求日志中间件,记录所有请求的核心信息(请求方法、路径、状态码、耗时、请求体大小等),输出到控制台(默认),方便开发调试和生产环境日志排查。
  2. 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")
}
  1. gin.Recovery():全局异常恢复中间件,捕获请求处理过程中的 panic 异常,防止程序崩溃退出,同时返回 HTTP 500 状态码给客户端,是生产环境必须注册的中间件(gin.Default() 已默认包含)。
  2. 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")
}
  1. 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")
}
  1. 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,})
	})
}

中间件注意事项

  1. 中间件中的协程:如果在中间件中需要启动新的 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)
    }()
}
  1. 洋葱模型执行顺序

    假设有一条路由注册了两个中间件 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 永远不会被执行。

  2. gin.Default() 默认使用了 gin.Logger()gin.Recovery() 中间件,即 gin.Default() 等价于 gin.New() 加上 r.Use(gin.Logger(), gin.Recovery())

相关推荐
席万里8 小时前
基于Go和Vue快速开发的博客系统-快速上手Gin框架
vue.js·golang·gin
只是懒得想了9 小时前
用Go通道实现并发安全队列:从基础到最佳实践
开发语言·数据库·golang·go·并发安全
fenglllle1 天前
使用fyne做一个桌面ipv4网段计算程序
开发语言·go
娱乐我有1 天前
Gin Lee八年淬炼金嗓重返红馆,2026开年第一场「声波开运仪式」
gin
码界奇点2 天前
基于Wails框架的Ollama模型桌面管理系统设计与实现
go·毕业设计·llama·源代码管理
csdn_aspnet3 天前
Go语言常用算法深度解析:并发与性能的优雅实践
后端·golang·go
吴老弟i4 天前
Go 多版本管理实战指南
golang·go
Grassto4 天前
HTTP请求超时?大数据量下的网关超时问题处理方案,流式处理,附go语言实现
后端·http·golang·go
提笔了无痕6 天前
Web中Token验证如何实现(go语言)
前端·go·json·restful