https://www.liwenzhou.com/posts/Go/gin/

中文文档


一、下载与示例

下载并安装Gin:

1
go get -u github.com/gin-gonic/gin

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

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

func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// GET:请求方式;/hello:请求的路径
// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
r.GET("/hello", func(c *gin.Context) {
// c.JSON:返回JSON格式的数据
c.JSON(200, gin.H{
"message": "Hello world!",
})
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run()
}

将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/hello就能看到一串JSON字符串。





二、RESTful API

阮一峰 理解RESTful架构

REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。

简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。

  • GET用来获取资源
  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源。

只要API程序遵循了REST风格,那就可以称其为RESTful API。目前在前后端分离的架构中,前后端基本都是通过RESTful API来进行交互。

例如,我们现在要编写一个管理书籍的系统,我们可以查询对一本书进行查询、创建、更新和删除等操作,我们在编写程序的时候就要设计客户端浏览器与我们Web服务端交互的方式和路径。按照经验我们通常会设计成如下模式:

请求方法 URL 含义
GET /book 查询书籍信息
POST /create_book 创建书籍记录
POST /update_book 更新书籍信息
POST /delete_book 删除书籍信息

同样的需求我们按照RESTful API设计如下:

请求方法 URL 含义
GET /book 查询书籍信息
POST /book 创建书籍记录
PUT /book 更新书籍信息
DELETE /book 删除书籍信息

Gin框架支持开发RESTful API的开发。





三、Gin渲染

提前声明:在前后端分离的架构中,返回纯数据是更为常见和推荐的做法

3.1 HTML渲染

我们首先定义一个存放模板文件的templates文件夹,然后在其内部按照业务分别定义一个posts文件夹和一个users文件夹。 posts/index.html文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{{define "posts/index.html"}}
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>posts/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}

users/index.html文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{define "users/index.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>users/index</title>
</head>
<body>
{{.title}}
</body>
</html>
{{end}}

Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法进行HTML模板渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

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

func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/**/*")
//r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.html", gin.H{
"title": "posts/index",
})
})

r.GET("users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.html", gin.H{
"title": "users/index",
})
})

r.Run(":8080")
}

函数解释

LoadHTMLGlob()

1
r.LoadHTMLGlob("templates/**/*")
  • 功能LoadHTMLGlob()方法借助 Go 语言的filepath.Glob函数,按照指定的模式来匹配文件,进而加载所有符合条件的 HTML 模板文件。
  • 参数:该方法接收一个字符串参数,此参数为匹配文件的模式。在示例中,"templates/**/*"表示要加载templates文件夹及其所有子文件夹内的全部文件。
  • 优点:使用通配符能一次性加载多个模板文件,当项目中的模板文件较多时,可显著减少代码量。
  • 缺点:若模板文件较多,加载时间可能会延长。

LoadHTMLFiles()

1
r.LoadHTMLFiles("templates/posts/index.html", "templates/users/index.html")
  • 功能LoadHTMLFiles()方法用于逐个加载指定的 HTML 模板文件。
  • 参数:该方法接收多个字符串参数,每个参数代表一个模板文件的路径。在示例里,它会加载templates/posts/index.htmltemplates/users/index.html这两个文件。
  • 优点:可以精准地控制要加载的模板文件,仅加载必要的文件,能减少不必要的加载时间。
  • 缺点:当模板文件数量较多时,需要列出每个文件的路径,代码会变得冗长。




3.2 自定义模版函数

服务器端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"html/template"
"net/http"

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

func main() {
router := gin.Default()
// 定义自定义模板函数
router.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML {
return template.HTML(str)
},
})
// 加载模板文件
router.LoadHTMLFiles("./index.tmpl")

// 定义路由处理函数
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", "<a href='https://gxblogs.com'>GXblogs</a>")
})

// 启动服务器
router.Run(":8080")
}
  1. 创建 Gin 引擎router := gin.Default() 创建了一个默认的 Gin 引擎实例,该实例包含了一些常用的中间件,如日志记录和恢复中间件。
  2. 定义自定义模板函数router.SetFuncMap 方法用于设置自定义的模板函数。在这个例子中,我们定义了一个名为 safe 的函数,它接收一个字符串参数,并将其转换为 template.HTML 类型。template.HTML 类型告诉 Go 的模板引擎,这个字符串不需要进行 HTML 转义。
  3. 加载模板文件router.LoadHTMLFiles("./index.tmpl") 加载了指定的模板文件 index.tmpl
  4. 定义路由处理函数router.GET("/index", ...) 定义了一个处理 /index 路径的 GET 请求的路由处理函数。在这个函数中,我们使用 c.HTML 方法渲染 index.tmpl 模板,并将一个 HTML 字符串作为数据传递给模板。
  5. 启动服务器router.Run(":8080") 启动了一个 HTTP 服务器,监听 :8080 端口。

HTML 模板文件

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>修改模板引擎的标识符</title>
</head>
<body>
<div>{{ . | safe }}</div>
</body>
</html>

index.tmpl 模板文件中,我们使用了自定义的 safe 函数。{{ . | safe }} 表示将传递给模板的数据(即 <a href='https://gxblogs.com'>GXblogs</a>)作为参数传递给 safe 函数进行处理。由于 safe 函数将字符串转换为 template.HTML 类型,模板引擎不会对该字符串进行 HTML 转义,而是直接将其作为 HTML 代码渲染到页面上。





3.3 模版渲染规则

在 Gin 框架结合 Go 模板引擎进行模板渲染时,这些内容存在特定的对应规则,下面详细解释:

数据传递格式

gin.H{ "title": "users/index" }

gin.H 是 Gin 框架里用于快速创建 map[string]interface{} 类型数据的便捷方式。像 gin.H{ "title": "users/index" } 这样的代码,就创建了一个映射,其中键是 "title",对应的值为 "users/index"。在使用 c.HTML 方法渲染模板时,这个映射会被当作数据传递给模板。

"<a href='https://gxblogs.com'>GXblogs</a>"

这是一个普通的字符串,在使用 c.HTML 方法渲染模板时,该字符串会作为数据传递给模板。它和 gin.H 不同,gin.H 是键值对形式的数据,而这个字符串可以直接作为模板的数据。


模板变量引用规则

.title

在 Go 模板里,当使用 gin.H 传递数据时,通过 . 来引用传递的数据对象。如果传递的数据是一个 map,那么可以用 .键名 的方式来引用 map 中的值。例如,当使用 c.HTML 传递 gin.H{ "title": "users/index" } 时,在模板中使用 .title 就能获取到 "users/index" 这个值。

以下是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"net/http"

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

func main() {
r := gin.Default()
r.LoadHTMLFiles("index.tmpl")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "users/index",
})
})
r.Run(":8080")
}

对应的 index.tmpl 模板文件:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{.title}}</title>
</head>
<body>
<h1>{{.title}}</h1>
</body>
</html>

在这个模板里,{{.title}} 会被替换成 "users/index"

.

当传递的是单个值(像字符串、整数等)时,在模板中使用 . 就可以引用这个值。比如,当使用 c.HTML 传递 "<a href='https://gxblogs.com'>GXblogs</a>" 时,在模板里使用 . 就能获取到这个字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"net/http"

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

func main() {
r := gin.Default()
r.LoadHTMLFiles("index.tmpl")
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", "<a href='https://gxblogs.com'>GXblogs</a>")
})
r.Run(":8080")
}

对应的 index.tmpl 模板文件:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Page</title>
</head>
<body>
{{.}}
</body>
</html>

在这个模板里,{{.}} 会被替换成 "<a href='https://gxblogs.com'>GXblogs</a>"


综上所述,在模板中 . 代表传递的数据对象,若传递的是 map,就可以用 .键名 引用具体的值;若传递的是单个值,直接用 . 引用该值。


ps: 在之前的代码示例里,可能没看到 safe 函数起作用,下面详细分析并给出能体现其作用的示例。

safe 函数的主要作用是告诉 Go 的模板引擎,传入的字符串不需要进行 HTML 转义,可直接当作 HTML 代码渲染。但如果没有正确使用该函数,或者传入的数据已经被转义过,就可能看不到其效果。

未使用自定义转义函数的效果:





3.4 自定义分隔符

1
router.Delims("{[{", "}]}")

在 Gin 框架中,Delims 方法允许你自定义模板引擎所使用的分隔符。默认情况下,Go 语言的模板引擎使用 {{` 和 `}} 作为分隔符,不过借助 Delims 方法,你能够把它们替换成自定义的分隔符,例如 {[ {} ]}

下面是一个完整的示例,展示了怎样使用自定义分隔符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"html/template"
"net/http"

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

func main() {
router := gin.Default()
// 自定义分隔符
router.Delims("{[{", "}]}")
// 加载模板文件
router.LoadHTMLGlob("templates/**/*")

// 定义自定义模板函数
router.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML {
return template.HTML(str)
},
})

// 定义路由处理函数
router.GET("/", func(c *gin.Context) {
htmlStr := "<a href='https://gxblogs.com'>GXblogs</a>"
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"htmlContent": htmlStr,
})
})

// 启动服务器
router.Run(":8080")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Using Custom Delims</title>
</head>
<body>
<!-- 不使用 safe 函数,HTML 标签会被转义 -->
<p>Without safe function: {[ {.htmlContent} ]}</p>
<!-- 使用 safe 函数,HTML 标签不会被转义,直接渲染 -->
<p>With safe function: {[ {.htmlContent | safe} ]}</p>
</body>
</html>

代码解释

  1. 自定义分隔符:在 main 函数里,调用 router.Delims("{[{", "}]}") 把模板引擎的分隔符设置成 {[ {} ]}
  2. 加载模板文件:使用 router.LoadHTMLGlob("templates/**/*") 加载 templates 目录下的所有模板文件。
  3. 定义自定义模板函数:和之前一样,定义了 safe 函数用于防止 HTML 转义。
  4. 路由处理函数:处理 / 路径的 GET 请求,将包含 HTML 标签的字符串传递给模板。
  5. 模板文件:在 templates/index.tmpl 文件中,使用自定义的分隔符 {[ {} ]} 来引用数据和调用模板函数。




3.5 扩展名的选择

在 Gin 框架中,.html.tmpl都是常用的模板文件扩展名,它们的使用主要取决于项目的约定和个人偏好,并没有严格的规定。以下是一些关于它们的说明和最佳实践:

扩展名的选择

  • .html:通常用于表示 HTML 模板文件,具有广泛的认知度和通用性。如果你的项目主要侧重于 Web 开发,并且模板文件主要是标准的 HTML 内容,使用.html扩展名可以让代码更易理解,与传统的 Web 开发文件类型保持一致,便于其他开发人员识别和处理。
  • .tmpl:常被用来明确表示这是一个模板文件,强调其作为模板的用途,与普通的 HTML 文件有所区别。当项目中存在多种类型的 HTML 相关文件,如静态 HTML 文件和模板文件时,使用.tmpl可以更清晰地将模板文件区分出来,有助于代码的组织和管理。

最佳实践

  • 保持一致性:在一个项目中,建议统一使用一种扩展名来表示模板文件,无论是.html还是.tmpl,避免混用,以保持代码风格的一致性和可维护性。
  • 根据项目特点选择:如果项目更注重与传统 Web 开发的融合,或者模板文件与普通 HTML 文件差异不大,可选择.html;如果希望突出模板文件的特殊性,或者项目中模板文件的处理较为复杂,与其他 HTML 文件有明显区别,那么.tmpl可能是更好的选择。
  • 考虑团队习惯:如果团队成员对某种扩展名有特定的偏好或经验,也可以根据团队的整体情况来决定,以便提高开发效率和代码的可读性。

在 Gin 框架中,使用LoadHTMLGlob()LoadHTMLFiles()方法加载模板文件时,会根据指定的扩展名来查找和解析相应的模板文件,所以无论选择哪种扩展名,只要在加载模板时正确指定,都能正常工作。

既然.html和.tmpl的效果一样,为什么还要有.tmpl?

在 Gin 框架中,.html.tmpl 作为模板文件扩展名,在功能上确实较为相似,但使用 .tmpl 也有其合理性:

  • 约定和区分:使用 .tmpl 扩展名可以明确表示该文件是一个模板文件,与普通的 HTML 文件区分开来。这有助于在项目中建立清晰的约定,让开发者一眼就能识别出哪些文件是用于模板渲染的,哪些是静态的 HTML 文件。特别是在大型项目中,当存在大量的 HTML 相关文件时,这种区分可以提高代码的可读性和可维护性。
  • 遵循传统:Go 语言的标准库 text/templatehtml/template 通常使用 .tmpl 作为模板文件的扩展名,许多基于 Go 的模板系统也遵循了这一惯例。Gin 框架在一定程度上遵循了 Go 语言的传统和习惯用法,所以也支持 .tmpl 扩展名,这使得熟悉 Go 标准库模板的开发者能够更自然地在 Gin 项目中使用模板。
  • 工具支持:虽然一些 IDE 可能默认不会直接识别 .tmpl 文件,但大多数现代 IDE 都支持自定义文件类型关联和语法高亮等功能。通过简单的配置,就可以让 IDE 正确识别 .tmpl 文件,将其作为模板文件进行语法检查、代码提示等,从而提高开发效率。此外,一些专门的模板开发工具和插件可能更倾向于使用 .tmpl 扩展名,使用该扩展名可以更好地与这些工具集成。

.html.tmpl 扩展名的选择在很大程度上取决于个人偏好和项目的具体需求。如果更注重与普通 HTML 文件的区分以及遵循 Go 语言的传统习惯,那么 .tmpl 是一个不错的选择;如果希望与现有的 HTML 文件保持一致,或者更习惯使用 .html 扩展名,也完全可以在 Gin 项目中使用 .html 作为模板文件的扩展名,Gin 框架对两者都提供了良好的支持。


问题:既然没有区别,为什么之前的HTML格式中加了

下一小节解释





3.6 模版复用

下面通过一个具体的 Gin 项目示例,来展示如何使用 {{define}}{{end}} 进行模板复用和代码组织,同时也会涉及到 .html.tmpl 文件的使用情况,这里以 .html 为例(使用 .tmpl 也是同样的逻辑)。


假设我们要开发一个简单的博客系统,有用户信息展示页面和文章详情页面,这两个页面都需要显示共同的头部和底部信息。

项目结构

1
2
3
4
5
6
7
8
- templates
- common
- header.html // 定义页面头部的模板
- footer.html // 定义页面底部的模板
- users
- userInfo.html // 用户信息展示页面的模板
- posts
- postDetail.html // 文章详情页面的模板

header.html 模板文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{define "common/header.html"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{.PageTitle}}</title>
<!-- 引入公共的 CSS 样式和 JavaScript 脚本 -->
<link rel="stylesheet" href="/styles/main.css">
</head>
<body>
<header>
<h1>我的博客系统</h1>
</header>
{{end}}

footer.html 模板文件内容

1
2
3
4
5
6
7
{{define "common/footer.html"}}
<footer>
<p>&copy; 2024 版权所有</p>
</footer>
</body>
</html>
{{end}}

userInfo.html 模板文件内容

1
2
3
4
5
6
7
8
9
{{define "users/userInfo.html"}}
{{template "common/header.html" .}} <!-- 引入头部模板 -->
<div class="user-info">
<h2>用户信息</h2>
<p>用户名:{{.UserName}}</p>
<p>邮箱:{{.UserEmail}}</p>
</div>
{{template "common/footer.html" .}} <!-- 引入底部模板 -->
{{end}}

postDetail.html 模板文件内容

1
2
3
4
5
6
7
8
{{define "posts/postDetail.html"}}
{{template "common/header.html" .}} <!-- 引入头部模板 -->
<div class="post-detail">
<h2>{{.PostTitle}}</h2>
<p>{{.PostContent}}</p>
</div>
{{template "common/footer.html" .}} <!-- 引入底部模板 -->
{{end}}

Gin 服务器端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"net/http"

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

func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/**/*")

// 用户信息展示页面路由处理函数
r.GET("/users/info", func(c *gin.Context) {
data := gin.H{
"PageTitle": "用户信息页面",
"UserName": "John Doe",
"UserEmail": "johndoe@example.com",
}
c.HTML(http.StatusOK, "users/userInfo.html", data)
})

// 文章详情页面路由处理函数
r.GET("/posts/detail", func(c *gin.Context) {
data := gin.H{
"PageTitle": "文章详情页面",
"PostTitle": "示例文章标题",
"PostContent": "这是文章的具体内容。",
}
c.HTML(http.StatusOK, "posts/postDetail.html", data)
})

r.Run(":8080")
}

在这个示例中,通过 {{define}}{{end}} 定义了不同的模板,并且在需要的地方通过 {{template}} 指令来复用这些模板,使得代码更加简洁和易于维护。无论是 .html 还是 .tmpl 文件,都可以按照这样的方式来使用 Go 模板引擎的相关语法进行模板的定义和使用。





3.7 模版继承

为何使用模板继承

在 Web 开发中,许多页面会有共同的结构和样式,如统一的头部、导航栏、底部版权信息等。重复编写这些相同部分不仅繁琐,而且后期维护困难,一处修改需在多个文件中同步调整。模板继承允许开发者创建一个基础模板,包含这些公共部分,其他模板继承基础模板并按需定制独特内容,从而提高代码复用性,降低维护成本,保持页面风格一致性。


实现步骤

(一)准备依赖

Gin 框架默认支持单模板,若要实现模板继承的block template功能,需引入github.com/gin-contrib/multitemplate库。通过go get github.com/gin-contrib/multitemplate命令获取该库。

(二)项目目录结构规划

推荐的项目目录结构如下:

1
2
3
4
5
6
7
templates
├── includes
│ ├── specificPage1.tmpl
│ └── specificPage2.tmpl
├── layouts
│ └── base.tmpl
└── otherTemplates.tmpl
  • base.tmpl:作为基础模板,放置所有页面共有的 HTML 结构、CSS 样式链接、JavaScript 脚本链接等内容。例如,包含页面的整体布局、通用的头部导航栏和底部版权信息。
  • specificPage1.tmplspecificPage2.tmpl:这些是具体页面的模板,继承自base.tmpl,主要编写每个页面独特的内容部分。
  • otherTemplates.tmpl:可用于存放其他通用的模板片段,供基础模板或具体页面模板复用。

(三)编写模板文件

  1. 基础模板(base.tmpl)
    使用{{block}}指令定义可被覆盖的区域。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{block "title" .}}默认标题{{end}}</title>
<link rel="stylesheet" href="/styles/main.css">
</head>
<body>
<header>
<h1>我的网站</h1>
<nav>
<ul>
<li><a href="/">首页</a></li>
<li><a href="/about">关于</a></li>
</ul>
</nav>
</header>
{{block "content" .}}
<p>默认内容,若子模板未覆盖则显示此内容</p>
{{end}}
<footer>
<p>&copy; 2024版权所有</p>
</footer>
<script src="/scripts/main.js"></script>
</body>
</html>

{{block "title" .}}默认标题{{end}}定义了title块,子模板可覆盖该块来设置特定页面标题。{{block "content" .}}...{{end}}定义了主要内容区域,子模板可在此处填充各自的内容。

  1. 子模板(以 specificPage1.tmpl 为例)
    使用{{define}}指令来覆盖基础模板中的{{block}}区域,并通过{{template}}指令引用基础模板。例如:
1
2
3
4
5
6
7
8
{{define "specificPage1.tmpl"}}
{{template "base.tmpl" .}}
{{define "title"}}特定页面1的标题{{end}}
{{define "content"}}
<h2>这是特定页面1的独特内容</h2>
<p>可以包含页面特有的文本、图片、链接等元素</p>
{{end}}
{{end}}

{{define "specificPage1.tmpl"}}定义了该模板的名称。{{template "base.tmpl" .}}表示继承base.tmpl。通过重新定义titlecontent块,实现对基础模板相应区域的定制。

(四)在 Go 代码中加载和使用模板

  1. 定义加载模板的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func loadTemplates(templatesDir string) multitemplate.Renderer {
r := multitemplate.NewRenderer()
layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
if err != nil {
panic(err.Error())
}
includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl")
if err != nil {
panic(err.Error())
}
for _, include := range includes {
layoutCopy := make([]string, len(layouts))
copy(layoutCopy, layouts)
files := append(layoutCopy, include)
r.AddFromFiles(filepath.Base(include), files...)
}
return r
}
  • multitemplate.NewRenderer():创建一个新的multitemplate.Renderer实例,用于管理多个模板。
  • filepath.Glob:获取指定目录下的所有.tmpl文件。分别获取layouts目录(存放基础模板)和includes目录(存放子模板)下的所有模板文件。
  • 循环遍历includes目录下的每个子模板文件:
    • 复制layouts切片,避免后续操作修改原始切片。
    • 将当前子模板文件添加到复制的layouts切片中,形成一个包含基础模板和当前子模板的新切片files
    • 使用r.AddFromFiles(filepath.Base(include), files...)将这些文件添加到Renderer中,其中filepath.Base(include)作为模板名称,files...是该模板包含的所有文件。
  1. 在 main 函数中配置和使用模板
1
2
3
4
5
6
7
8
func main() {
r := gin.Default()
r.HTMLRender = loadTemplates("./templates")
r.GET("/specificPage1", func(c *gin.Context) {
c.HTML(http.StatusOK, "specificPage1.tmpl", nil)
})
r.Run(":8080")
}
  • r := gin.Default():创建一个默认的 Gin 引擎实例。
  • r.HTMLRender = loadTemplates("./templates"):将loadTemplates函数返回的Renderer实例赋值给 Gin 引擎的HTMLRender属性,让 Gin 知道如何渲染这些模板。
  • r.GET("/specificPage1", func(c *gin.Context) {... }):定义路由,当访问/specificPage1时,使用c.HTML方法渲染specificPage1.tmpl模板。
  • r.Run(":8080"):启动 Gin 服务器,监听8080端口。

三、注意事项

  1. 模板名称的一致性:在loadTemplates函数中通过filepath.Base(include)设置的模板名称,要与在c.HTML方法中使用的模板名称完全一致,否则无法正确渲染模板。
  2. 块定义与覆盖的顺序:在子模板中,覆盖基础模板的{{block}}区域的{{define}}指令应在{{template}}指令之后,且{{define}}指令内的块名称要与基础模板中的{{block}}名称一致。
  3. 数据传递:在渲染模板时(如c.HTML方法)传递的数据,可以在基础模板和子模板中使用 Go 模板语法进行访问和展示。子模板继承基础模板的数据访问规则,且可以在其覆盖的块中根据需要进一步处理和展示数据。

评价模版继承

在上述代码中,一个路由引擎确实只有一个模板基类(base.tmpl),这种设计有其合理性和局限性,具体分析如下:

合理性

  • 统一页面布局:在大多数网页应用中,通常希望多个页面具有统一的布局,如相同的导航栏、页脚等。通过使用单一的模板基类,可以方便地在一个地方定义这些公共部分,确保所有继承该基类的页面具有一致的外观和风格,有利于保持网站的整体一致性。
  • 易于维护:当需要对网站的整体布局进行修改时,只需在基类模板中进行更改,所有继承该基类的页面都会自动应用这些修改,无需逐个修改每个页面的相关代码,大大提高了维护效率。

局限性

  • 缺乏灵活性:如果有一些特殊页面需要完全不同的布局,使用单一基类可能会受到限制。因为所有页面都继承自同一个基类,要实现特殊布局可能需要额外的处理或对基类进行复杂的条件判断,这可能会增加代码的复杂性。
  • 功能扩展受限:随着项目的发展,如果需要引入新的布局风格或对不同类型的页面进行更细致的布局管理,单一基类可能无法很好地满足需求。可能需要对代码结构进行较大的调整才能实现新的布局要求。

如果项目中存在多种不同类型的页面,且它们的布局差异较大,可能需要考虑更灵活的模板设计方案,例如使用多个基类模板,或者根据不同的路由或业务需求动态选择合适的基类模板来渲染页面。但对于一些布局相对统一的小型项目或特定场景,使用一个模板基类可以有效地提高开发效率和代码的可维护性。





3.8 静态文件

在使用 Gin 框架构建 Web 应用时,处理静态文件(如 CSS、JavaScript、图片等)是很常见的需求。下面为你详细解释这段代码中静态文件处理的部分,以及在 HTML 文件中如何引用这些静态文件。


代码解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

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

func main() {
// 创建一个默认的Gin引擎实例,默认包含日志和恢复中间件
r := gin.Default()

// 设置静态文件服务
r.Static("/static", "./static")

// 加载模板文件,支持通配符,这里会加载 templates 目录下的所有文件
r.LoadHTMLGlob("templates/**/*")

// 这里可以添加路由处理函数

// 启动服务器,监听 8080 端口
r.Run(":8080")
}
  • r.Static("/static", "./static"):这行代码的作用是将访问路径 /static 映射到本地文件系统中的 ./static 目录。当客户端请求以 /static 开头的 URL 时,Gin 会从 ./static 目录中查找对应的文件并返回。例如,如果客户端请求 /static/css/style.css,Gin 会尝试从 ./static/css/style.css 文件中读取内容并返回给客户端。
  • r.LoadHTMLGlob("templates/**/*"):这行代码用于加载模板文件。templates/**/* 是一个通配符表达式,表示加载 templates 目录下的所有文件,包括子目录中的文件。这样,后续在处理路由时就可以使用这些模板文件进行 HTML 渲染。

在 HTML 文件中引用静态文件

假设你的项目结构如下:

1
2
3
4
5
6
7
8
9
project/
├── main.go
├── static/
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── script.js
└── templates/
└── index.html

templates/index.html 文件中,你可以按照以下方式引用静态文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Website</title>
<!-- 引用 CSS 文件 -->
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1>Welcome to my website</h1>
<!-- 引用 JavaScript 文件 -->
<script src="/static/js/script.js"></script>
</body>
</html>

在上述 HTML 代码中,通过 /static 前缀来引用静态文件,Gin 会根据之前设置的静态文件映射规则,从对应的本地目录中查找并返回这些文件。


注意事项

  • 确保 ./static 目录存在,并且包含你需要提供的静态文件。
  • 静态文件的路径是相对于项目根目录的,因此要注意文件的实际位置。
  • 如果需要修改静态文件的访问路径或本地目录,只需修改 r.Static 方法的参数即可。




3.9 返回json格式数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
r := gin.Default()

// gin.H 是map[string]interface{}的缩写
r.GET("/someJSON", func(c *gin.Context) {
// 方式一:自己拼接JSON
c.JSON(http.StatusOK, gin.H{"message": "Hello world!"})
})
r.GET("/moreJSON", func(c *gin.Context) {
// 方法二:使用结构体
var msg struct {
Name string `json:"user"`
Message string
Age int
}
msg.Name = "小王子"
msg.Message = "Hello world!"
msg.Age = 18
c.JSON(http.StatusOK, msg)
})
r.Run(":8080")
}
  • 作用:通过 Gin 框架的c.JSON方法将数据转换为 JSON 格式并返回给客户端。
  • 方式一:使用gin.H(即map[string]interface{})直接拼接 JSON 数据,简单直接,适用于快速构建简单的 JSON 响应。
  • 方式二:定义结构体,将数据存储在结构体中,再通过c.JSON方法将结构体转换为 JSON 格式。结构体中的json:"user"标签用于指定 JSON 字段名,可实现结构体字段名与 JSON 字段名的映射。




四、获取参数

4.1 获取querystring参数

querystring指的是URL中?后面携带的参数,例如:/user/search?username=小王子&address=沙河。 获取请求的querystring参数的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.GET("/user/search", func(c *gin.Context) {
username := c.DefaultQuery("username", "小王子")
//username := c.Query("username")
address := c.Query("address")
//输出json结果给调用方
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run()
}
  • c.DefaultQuery(key, defaultValue string) string:用于获取指定名称的查询参数,若参数不存在则返回默认值。
  • c.Query(key string) string:用于获取指定名称的查询参数,若参数不存在则返回空字符串。




4.2 获取form参数

当前端请求的数据通过form表单提交时,例如向/user/search发送一个POST请求,获取请求数据的方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
r := gin.Default()
r.POST("/user/search", func(c *gin.Context) {
// 使用 PostForm 方法获取表单参数
username := c.PostForm("username")
// 也可使用 DefaultPostForm 方法,若取不到值返回默认值
// username := c.DefaultPostForm("username", "小王子")
address := c.PostForm("address")
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})
r.Run(":8080")
}
  • c.PostForm(key string) string:用于获取表单中指定名称的参数值,若参数不存在则返回空字符串。
  • c.DefaultPostForm(key, defaultValue string) string:同样用于获取表单参数,若参数不存在则返回指定的默认值。




4.3 获取JSON参数

当前端请求的数据通过JSON提交时,例如向/json发送一个JSON格式的POST请求,则获取请求参数的方式如下:

1
2
3
4
5
6
7
8
9
10
r.POST("/json", func(c *gin.Context) {
// 注意:下面为了举例子方便,暂时忽略了错误处理
b, _ := c.GetRawData() // 从c.Request.Body读取请求数据
// 定义map或结构体
var m map[string]interface{}
// 反序列化
_ = json.Unmarshal(b, &m)

c.JSON(http.StatusOK, m)
})
  • 读取原始数据c.GetRawData()c.Request.Body 中读取请求携带的原始数据,返回的是字节切片 []byte。这里为了简化示例,忽略了错误处理。

  • 定义存储结构var m map[string]interface{} 定义了一个 map,键为 string 类型,值为 interface{} 类型,用于存储反序列化后的 JSON 数据。也可以使用结构体来存储,但需要提前定义好结构体的字段和类型。





4.4 获取path参数

请求的参数通过URL路径传递,例如:/user/search/小王子/沙河。 获取请求URL路径中的参数的方式如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
// 创建默认的 Gin 路由引擎,包含日志和恢复中间件
r := gin.Default()

// 定义一个处理 GET 请求的路由,路径中包含动态参数
r.GET("/user/search/:username/:address", func(c *gin.Context) {
// 从 URL 路径中获取 username 参数的值
username := c.Param("username")
// 从 URL 路径中获取 address 参数的值
address := c.Param("address")

// 以 JSON 格式返回响应给客户端
c.JSON(http.StatusOK, gin.H{
"message": "ok",
"username": username,
"address": address,
})
})

// 启动 Gin 服务器,监听 8080 端口
r.Run(":8080")
}
  • 定义路由r.GET("/user/search/:username/:address", ...) 定义了一个 GET 请求的路由,其中 :username:address 是动态路径参数。当客户端发送符合该路径格式的请求时,会执行对应的处理函数。

  • 获取路径参数c.Param("username")c.Param("address") 用于从 URL 路径中提取 usernameaddress 参数的值。





4.5 参数绑定(反射)

在 Go 的 Gin 框架开发中,为高效获取请求相关参数,可利用反射机制根据请求的 Content-Type 识别数据类型,将 QueryString、form 表单、JSON、XML 等参数自动提取到结构体中。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"net/http"

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

// Login 结构体用于绑定请求参数
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}

func main() {
router := gin.Default()

// 处理 POST 请求,绑定 JSON 数据
router.POST("/loginJSON", func(c *gin.Context) {
var login Login
if err := c.ShouldBind(&login); err == nil {
fmt.Printf("login info:%#v\n", login)
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})

// 处理 POST 请求,绑定 form 表单数据
router.POST("/loginForm", func(c *gin.Context) {
var login Login
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})

// 处理 GET 请求,绑定 QueryString 数据
router.GET("/loginForm", func(c *gin.Context) {
var login Login
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})

router.Run(":8080")
}

代码关键部分解释

  1. 结构体定义:定义 Login 结构体,通过标签 formjson 指定在不同数据格式下的字段名,binding:"required" 表示该字段为必需项。

  2. 路由处理:

    • /loginJSON:处理 POST 请求,绑定 JSON 数据。若绑定成功,返回用户信息;若失败,返回错误信息。
    • /loginForm(POST):处理 POST 请求,绑定 form 表单数据。绑定逻辑与 /loginJSON 类似。
    • /loginForm(GET):处理 GET 请求,绑定 QueryString 数据。同样根据绑定结果返回相应信息。
  3. ShouldBind 方法:该方法会依据请求的

    1
    Content-Type

    自动选择合适的绑定器。

    • GET 请求:仅使用 Form 绑定引擎(query)。
    • POST 请求:先检查 content-type 是否为 JSON 或 XML,若不是则使用 Form(form-data)。
Form 绑定引擎(query)

在 Go 的 Gin 框架中,Form 绑定引擎(query)是一种用于处理 HTTP 请求中查询字符串(QueryString)参数的机制,下面从概念、工作原理、使用示例等方面详细介绍。


概念

在 HTTP 请求里,GET 请求常把参数附加到 URL 的查询字符串中,查询字符串位于 URL 的 ? 之后,参数间用 & 分隔。Form 绑定引擎(query)的作用就是解析这些查询字符串参数,并将其绑定到 Go 结构体的字段上,让开发者能更便捷地处理请求参数。


工作原理

  1. 解析查询字符串:当接收到 GET 请求时,Form 绑定引擎会从 URL 中提取查询字符串,然后把它拆分成一个个键值对。
  2. 结构体标签匹配:在 Go 结构体中,开发者可以使用 form 标签来指定每个字段对应的查询字符串参数名。Form 绑定引擎会依据这些标签,将解析出的键值对与结构体字段进行匹配。
  3. 数据类型转换:引擎会尝试把查询字符串中的值转换为结构体字段对应的数据类型。例如,若结构体字段是 int 类型,引擎会把查询字符串中的值转换为整数。
  4. 绑定到结构体:完成匹配和类型转换后,引擎会把解析和转换后的值赋给结构体的相应字段。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"net/http"

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

// User 定义一个结构体用于绑定查询字符串参数
type User struct {
Name string `form:"name"`
Age int `form:"age"`
}

func main() {
r := gin.Default()

r.GET("/user", func(c *gin.Context) {
var user User
// 使用 ShouldBind 方法,对于 GET 请求会使用 Form 绑定引擎(query)
if err := c.ShouldBind(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"name": user.Name,
"age": user.Age,
})
})

r.Run(":8080")
}

在这个示例中:

  • 定义了 User 结构体,form 标签指定了查询字符串中参数与结构体字段的对应关系。
  • 当客户端发送 GET 请求到 /user?name=Alice&age=25 时,ShouldBind 方法会调用 Form 绑定引擎(query)来解析查询字符串。
  • 引擎将 name 参数的值 Alice 绑定到 User 结构体的 Name 字段,把 age 参数的值 25 绑定到 Age 字段。
  • 若绑定成功,服务器返回包含用户信息的 JSON 响应;若失败,返回包含错误信息的 JSON 响应。

总结

Form 绑定引擎(query)是 Gin 框架中处理 GET 请求查询字符串参数的重要工具,它通过解析、匹配和类型转换等步骤,将查询字符串参数绑定到结构体字段,简化了开发者处理请求参数的过程。

绑定函数 适用数据格式 功能特点
ShouldBind JSON、XML、Form 等 根据请求的 Content-Type 自动选择合适的绑定器解析数据并绑定到结构体,绑定失败返回错误给调用者处理。
Bind JSON、XML、Form 等 根据请求的 Content-Type 自动选择合适的绑定器解析数据并绑定到结构体,绑定失败直接向客户端返回 400 Bad Request 响应。
ShouldBindJSON JSON 专门将请求体中的 JSON 数据绑定到结构体,绑定失败返回错误给调用者处理。
BindJSON JSON 专门将请求体中的 JSON 数据绑定到结构体,绑定失败直接向客户端返回 400 Bad Request 响应。
ShouldBindXML XML 专门将请求体中的 XML 数据绑定到结构体,绑定失败返回错误给调用者处理。
BindXML XML 专门将请求体中的 XML 数据绑定到结构体,绑定失败直接向客户端返回 400 Bad Request 响应。
ShouldBindQuery URL 查询字符串(Query) 从 URL 的查询字符串中提取参数并绑定到结构体,绑定失败返回错误给调用者处理。
BindQuery URL 查询字符串(Query) 从 URL 的查询字符串中提取参数并绑定到结构体,绑定失败直接向客户端返回 400 Bad Request 响应。
ShouldBindForm Form(application/x-www-form-urlencodedmultipart/form-data 将表单数据(包括 application/x-www-form-urlencodedmultipart/form-data 格式)绑定到结构体,绑定失败返回错误给调用者处理。
BindForm Form(application/x-www-form-urlencodedmultipart/form-data 将表单数据(包括 application/x-www-form-urlencodedmultipart/form-data 格式)绑定到结构体,绑定失败直接向客户端返回 400 Bad Request 响应。

用带should的就行,类似上面的处理。





五、文件上传

这里介绍了使用 Go 的 Gin 框架实现文件上传功能,包含单个文件上传和多个文件上传的具体实现,同时给出了对应的前端页面代码。


前端代码

前端通过 HTML 表单实现文件选择与上传功能,关键要点如下:

  1. 表单设置:使用 POST 方法和 enctype="multipart/form-data",这是文件上传必需的配置。
  2. 文件选择:通过 <input type="file" name="f1"> 让用户选择要上传的文件。
  3. 提交按钮<input type="submit" value="上传"> 用于提交表单。
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>上传文件示例</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="f1">
<input type="submit" value="上传">
</form>
</body>
</html>

后端代码

(一)单个文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// 单个文件
file, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": err.Error(),
})
return
}

log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
})
})
router.Run()
}
  1. 路由设置:使用 router.POST("/upload", ...) 处理文件上传请求。
  2. 获取文件:通过 c.FormFile("f1") 获取上传的单个文件,若出现错误则返回 500 状态码和错误信息。
  3. 保存文件:使用 c.SaveUploadedFile(file, dst) 将文件保存到指定目录(这里是 C:/tmp)。
  4. 返回响应:上传成功后返回 200 状态码和上传成功的消息。
  5. 内存限制:可通过 router.MaxMultipartMemory 修改处理 multipart forms 提交文件时的内存限制。

(二)多个文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
router := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["file"]

for index, file := range files {
log.Println(file.Filename)
dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%d files uploaded!", len(files)),
})
})
router.Run()
}
  1. 路由设置:同样使用 router.POST("/upload", ...) 处理文件上传请求。
  2. 获取文件列表:通过 c.MultipartForm() 获取表单数据,从中提取所有上传的文件(form.File["file"])。
  3. 循环保存文件:遍历文件列表,使用 c.SaveUploadedFile(file, dst) 将每个文件保存到指定目录,为避免文件名冲突,在文件名后添加索引。
  4. 返回响应:上传成功后返回 200 状态码和上传文件数量的消息。
  5. 内存限制:同单个文件上传,可修改内存限制。

注意事项

  1. 错误处理:实际开发中要完善错误处理逻辑,涵盖获取文件和保存文件时可能出现的各类错误。
  2. 文件命名:多个文件上传时,要考虑文件名冲突问题,可采用添加索引等方式解决。
  3. 内存管理:根据实际情况调整 router.MaxMultipartMemory 的值,防止因内存不足导致上传失败。
  4. 文件路径权限:确保服务器有足够权限在指定路径下创建和写入文件。




六、重定向

6.1 HTTP 重定向

  1. 特点

支持内部和外部重定向,使用简单。

  1. 示例代码
1
2
3
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})
  1. 代码解释
  • 路由定义:使用 r.GET("/test", ...) 定义一个 GET 请求的路由。
  • 重定向操作c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/") 表示将客户端的请求永久重定向到 http://www.sogo.com/http.StatusMovedPermanently 是 HTTP 状态码 301,表示永久重定向;也可以使用其他状态码,如 http.StatusFound(302,临时重定向)。




6.2 路由重定向

  1. 特点

通过修改请求的 URL 路径,然后使用 HandleContext 方法将请求交给另一个路由处理,实现路由间的重定向。

  1. 示例代码
1
2
3
4
5
6
7
8
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"})
})
  1. 代码解释
  • 第一个路由 /test
    • 修改请求路径c.Request.URL.Path = "/test2" 将当前请求的 URL 路径修改为 /test2
    • 处理请求r.HandleContext(c) 把修改后的请求交给 Gin 框架的路由处理器,让其寻找匹配 /test2 的路由进行处理。
  • 第二个路由 /test2:当请求被重定向到 /test2 时,该路由处理函数会被执行,返回一个包含 {"hello": "world"} 的 JSON 响应。

总结

  • HTTP 重定向适用于将请求重定向到外部网站或其他 URL,使用 c.Redirect 方法,通过设置不同的 HTTP 状态码可实现永久或临时重定向。
  • 路由重定向用于在 Gin 框架内部将一个路由的请求重定向到另一个路由,通过修改请求的 URL 路径并使用 HandleContext 方法实现。




七、路由

7.1 路由

(一)、普通路由

Gin 框架提供了多种方法来定义不同 HTTP 请求方法的路由,可根据具体业务需求对不同请求方法进行处理。

  • GET 请求路由:用于处理客户端的 GET 请求,通常用于获取资源。
1
2
r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
  • POST 请求路由:常用于向服务器提交数据,如表单提交等操作。
1
r.POST("/login", func(c *gin.Context) {...})

(二)、Any 方法路由

Any 方法可以匹配所有的 HTTP 请求方法,包括 GET、POST、PUT、DELETE 等。当你希望某个路由对所有请求方法都进行相同的处理时,可使用此方法。

1
r.Any("/test", func(c *gin.Context) {...})

(三)、NoRoute 路由

NoRoute 方法用于为没有配置处理函数的路由添加处理程序。当客户端请求的路由在现有路由配置中未找到匹配时,会执行 NoRoute 中定义的处理逻辑。默认情况下,Gin 对未匹配的路由返回 404 状态码,可通过自定义 NoRoute 处理函数来返回特定的页面或信息。

1
2
3
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})

上述代码中,当请求的路由未匹配到时,会返回 views/404.html 页面给客户端,状态码为 404。


(四)、注意事项

  • 路由顺序:Gin 按路由定义的顺序匹配请求,因此要合理安排路由顺序,避免规则冲突。
  • 错误处理:在实际应用中,可在路由处理函数中添加错误处理逻辑,增强程序健壮性。
  • 路径参数:Gin 支持在路由路径中使用参数,可进一步扩展路由功能,实现动态路由。




7.2 路由组

(一)、路由组概念

在 Gin 框架里,可把拥有相同 URL 前缀的路由归为一个路由组。使用路由组能让代码结构更清晰,便于管理和维护不同业务逻辑或不同版本的 API。


(二)、基本路由组使用

通过 r.Group(prefix) 方法创建路由组,prefix 为该路由组的 URL 前缀,组内的路由路径会自动添加此前缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
r := gin.Default()
// 创建 user 路由组
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) {...})
}
// 创建 shop 路由组
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()
}
  • userGroup:URL 前缀为 /user,组内路由 /index 实际访问路径是 /user/index/login 实际路径是 /user/login
  • shopGroup:URL 前缀为 /shop,组内路由 /index 实际路径是 /shop/index/cart 实际路径是 /shop/cart 等。
  • 花括号作用:使用 {} 包裹同组路由主要是为了增强代码可读性,对功能无影响。

(三)、路由组嵌套

路由组支持嵌套,可进一步细化路由结构。

1
2
3
4
5
6
7
8
9
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) {...})
// 嵌套路由组
xx := shopGroup.Group("xx")
xx.GET("/oo", func(c *gin.Context) {...})
}
  • shopGroup:URL 前缀为 /shop
  • xx 嵌套路由组:它是 shopGroup 的子组,URL 前缀为 /shop/xx,组内路由 /oo 实际访问路径是 /shop/xx/oo

(四)、应用场景

  • 业务逻辑划分:如示例中的 user 路由组处理用户相关业务,shop 路由组处理商城相关业务,让代码按业务模块组织,提高可维护性。
  • API 版本划分:在开发不同版本的 API 时,可按版本号创建路由组,如 /v1/v2 等,方便管理和升级 API。




7.3 路由原理

(一)、基于 httprouter 库的路由原理

Gin 框架采用 httprouter 库来实现路由功能,其核心原理是构建一个路由地址的前缀树(也叫前缀树或字典树)结构。

  1. 前缀树构建:当定义路由时,比如有 /user/user/profile/user/settings 这些路由,httprouter 会将它们按照路径的前缀关系组织成一棵前缀树。树的节点代表路径中的一部分,从根节点到某个叶子节点的路径就对应一个完整的路由。例如,根节点可能代表根路径,/user 是根节点的一个子节点,/user/profile/user/settings 则是 /user 节点的子节点。
  2. 路由匹配:当客户端发起请求时,httprouter 会根据请求的 URL 路径在这棵前缀树上进行查找匹配。从根节点开始,依次比较路径的各个部分,找到匹配的节点。如果找到了完全匹配的叶子节点,就执行对应的路由处理函数;如果没有找到完全匹配的节点,但存在部分匹配的节点,也可能根据情况进行处理(比如返回 404 或其他错误)。
  3. 参数处理:对于包含参数的路由,如 /user/:idhttprouter 会在匹配过程中识别出参数部分,并将参数值提取出来,传递给路由处理函数。它通过在路径中使用特定的标记(如 : 后面跟着参数名)来区分参数和普通路径部分。

(二)、httprouter 库实现路由的优点

  1. 高效性:前缀树结构使得路由匹配的时间复杂度较低,通常为 O (n),其中 n 是路径的长度。这意味着在处理大量路由时,能够快速地找到匹配的路由,提高了路由匹配的性能。
  2. 灵活性:支持多种类型的路由,包括静态路由、带参数的路由等,能够满足不同应用场景的需求。同时,对于参数的处理也比较灵活,可以方便地在路由处理函数中获取参数值。
  3. 内存占用相对较小:相比于一些其他的路由实现方式,前缀树结构在存储路由信息时,能够有效地减少内存的占用,因为它共享了路径的前缀部分。




7.4 带参数的路由

在 Gin 框架里,带参数的路由能让你定义动态的路由规则,可处理包含可变部分的 URL,以下为你详细介绍带参数的路由以及参数规则设置方法:


带参数的路由类型及示例

  1. 命名参数
  • 规则:使用 : 来定义命名参数,在路由处理函数里可以通过 c.Param 方法获取参数值。
  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

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

func main() {
r := gin.Default()
// 定义带命名参数的路由
r.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.JSON(200, gin.H{
"message": "Hello, " + name,
})
})
r.Run(":8080")
}
  • 解释:当客户端请求 /user/john 时,name 参数的值为 john,处理函数会返回 {"message": "Hello, john"}
  1. 通配符参数
  • 规则:使用 * 来定义通配符参数,它可以匹配路径中剩余的任意部分。通配符参数必须是路由路径的最后一部分。
  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

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

func main() {
r := gin.Default()
// 定义带通配符参数的路由
r.GET("/files/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
c.JSON(200, gin.H{
"message": "File path: " + filepath,
})
})
r.Run(":8080")
}
  • 解释:当客户端请求 /files/documents/report.pdf 时,filepath 参数的值为 /documents/report.pdf,处理函数会返回 {"message": "File path: /documents/report.pdf"}

设置参数规则

  1. 正则表达式约束(借助中间件)

Gin 本身未直接支持正则表达式来约束参数,但可以通过自定义中间件实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"github.com/gin-gonic/gin"
"net/http"
"regexp"
)

// 自定义中间件来验证参数
func validateID(c *gin.Context) {
id := c.Param("id")
// 定义正则表达式规则,这里要求 id 是数字
match, _ := regexp.MatchString("^[0-9]+$", id)
if!match {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid ID format",
})
c.Abort()
return
}
c.Next()
}

func main() {
r := gin.Default()
// 应用中间件到带参数的路由
r.GET("/users/:id", validateID, func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{
"message": "User ID: " + id,
})
})
r.Run(":8080")
}
  • 解释:上述代码定义了一个 validateID 中间件,使用正则表达式 ^[0-9]+$ 验证 id 参数是否为纯数字。若不符合规则,返回 400 Bad Request 错误响应。
  1. 手动验证

在路由处理函数中手动验证参数的合法性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)

func main() {
r := gin.Default()
r.GET("/products/:id", func(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid product ID",
})
return
}
c.JSON(200, gin.H{
"message": "Product ID: " + strconv.Itoa(id),
})
})
r.Run(":8080")
}
  • 解释:在这个例子中,在路由处理函数里手动将 id 参数转换为整数,若转换失败则返回错误响应。




八、中间件

Gin 框架支持开发者在处理请求的流程中插入自定义的钩子函数,这些钩子函数被称为中间件。中间件适合处理公共业务逻辑,如登录认证、权限校验、数据分页、日志记录、耗时统计等。

8.1 中间件定义规则

Gin 中的中间件必须是 gin.HandlerFunc 类型,该类型是一个接收 *gin.Context 参数且无返回值的函数。

1
2
3
4
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

func(c *gin.Context)




8.2 常见中间件示例

> 记录接口耗时的中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
// 调用该请求的剩余处理程序
c.Next()
// 不调用该请求的剩余处理程序
// c.Abort()
// 计算耗时
cost := time.Since(start)
log.Println(cost)
}
}
  • 功能:统计每个请求的处理耗时并记录日志。
  • 实现步骤:
    • 在中间件函数开始处记录当前时间。
    • 使用 c.Set 方法在请求上下文中设置值,后续的处理函数可以通过 c.Get 方法获取该值。
    • 调用 c.Next() 方法继续执行后续的处理程序。
    • 计算从开始到当前的时间差,即请求处理耗时,并记录日志。
    • 若调用 c.Abort() 方法,则会终止后续处理程序的执行。




> 记录响应体的中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type bodyLogWriter struct {
gin.ResponseWriter // 嵌入gin框架ResponseWriter
body *bytes.Buffer // 我们记录用的response
}

// Write 写入响应体数据
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b) // 我们记录一份
return w.ResponseWriter.Write(b) // 真正写入响应
}

// ginBodyLogMiddleware 一个记录返回给客户端响应体的中间件
// https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin
func ginBodyLogMiddleware(c *gin.Context) {
blw := &bodyLogWriter{body: bytes.NewBuffer([]byte{}), ResponseWriter: c.Writer}
c.Writer = blw // 使用我们自定义的类型替换默认的

c.Next() // 执行业务逻辑

fmt.Println("Response body: " + blw.body.String()) // 事后按需记录返回的响应
}
  • 功能:记录返回给客户端的响应体数据。
  • 实现步骤:
    • 定义一个自定义的 bodyLogWriter 结构体,嵌入 gin.ResponseWriter 并添加一个 bytes.Buffer 用于记录响应体数据。
    • 实现 Write 方法,在该方法中先将响应体数据写入 bytes.Buffer 进行记录,再调用原始的 Write 方法将数据写入响应。
    • 在中间件函数中,创建 bodyLogWriter 实例并替换默认的 c.Writer
    • 调用 c.Next() 方法执行后续的处理程序。
    • 处理程序执行完毕后,从 bytes.Buffer 中获取记录的响应体数据并记录日志。




> 跨域中间件cors

跨域问题与解决方案

在前后端分离架构中,由于浏览器的同源策略,会出现跨域问题。为解决该问题,推荐使用社区的 github.com/gin-contrib/cors 库,该库能通过简单配置解决跨域问题,且中间件需注册在业务处理函数之前。


详细配置使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"time"

"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()
// CORS for https://foo.com and https://github.com origins, allowing:
// - PUT and PATCH methods
// - Origin header
// - Credentials share
// - Preflight requests cached for 12 hours
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://foo.com"}, // 允许跨域发来请求的网站
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, // 允许的请求方法
AllowHeaders: []string{"Origin", "Authorization", "Content-Type"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
AllowOriginFunc: func(origin string) bool { // 自定义过滤源站的方法
return origin == "https://github.com"
},
MaxAge: 12 * time.Hour,
}))
router.Run()
}

配置项说明:

  • AllowOrigins:指定允许跨域请求的源站列表。
  • AllowMethods:定义允许的请求方法,如 GETPOST 等。
  • AllowHeaders:设置允许的请求头。
  • ExposeHeaders:指定可以暴露给客户端的响应头。
  • AllowCredentials:若设为 true,则允许在跨域请求中携带凭证(如 cookie)。
  • AllowOriginFunc:自定义源站过滤函数,根据传入的源站字符串返回布尔值来决定是否允许该源站的请求。
  • MaxAge:预检请求(Preflight Request)的缓存时间,减少不必要的预检请求。

默认配置使用

1
2
3
4
5
6
7
8
9
func main() {
router := gin.Default()
// same as
// config := cors.DefaultConfig()
// config.AllowAllOrigins = true
// router.Use(cors.New(config))
router.Use(cors.Default())
router.Run()
}
  • 功能:使用默认配置,允许所有的跨域请求。这种方式简单直接,但在生产环境中可能存在安全风险,需谨慎使用。若需要更精细的跨域控制,建议使用详细配置的方式。

注意事项

  • 中间件注册顺序:跨域中间件必须注册在业务处理函数之前,以确保在处理业务逻辑之前先处理跨域相关的问题。
  • 安全考量:在使用 AllowAllOrigins 或自定义 AllowOriginFunc 时,要充分考虑安全问题,避免开放过多不必要的跨域权限。




8.3 注册中间件

在 Gin 框架里,能为每个路由添加任意数量的中间件,可根据不同需求为全局路由、单个路由或路由组注册中间件。

(一)、为全局路由注册中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
// 新建一个没有任何默认中间件的路由
r := gin.New()
// 注册一个全局中间件
r.Use(StatCost())

r.GET("/test", func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})
r.Run()
}

步骤:

  1. 使用 gin.New() 创建一个没有默认中间件的路由实例。
  2. 运用 r.Use(StatCost()) 注册全局中间件 StatCost(),这样所有路由都会经过该中间件处理。
  3. 定义具体路由,在处理函数中可通过 c.MustGet 从上下文获取中间件设置的值。

(二)、为某个路由单独注册中间件

1
2
3
4
5
6
7
8
// 给/test2路由单独注册中间件(可注册多个)
r.GET("/test2", StatCost(), func(c *gin.Context) {
name := c.MustGet("name").(string) // 从上下文取值
log.Println(name)
c.JSON(http.StatusOK, gin.H{
"message": "Hello world!",
})
})

特点:仅对 /test2 这个特定路由应用 StatCost() 中间件。若需要多个中间件,可依次罗列在路由处理函数之前,中间件会按顺序执行。


(三)、为路由组注册中间件

法一:

1
2
3
4
5
shopGroup := r.Group("/shop", StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
  • 步骤:在创建路由组 shopGroup 时,将中间件 StatCost() 作为参数传入,该路由组下的所有路由都会使用此中间件。

法二:

1
2
3
4
5
6
shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
shopGroup.GET("/index", func(c *gin.Context) {...})
...
}
  • 步骤:先创建路由组 shopGroup,再使用 shopGroup.Use(StatCost()) 为该路由组注册中间件,效果与写法 1 相同。

总结

  • 全局注册:适用于需要对所有请求进行统一处理的场景,如日志记录、请求耗时统计等。
  • 单个路由注册:用于仅对特定路由添加特殊处理逻辑的情况。
  • 路由组注册:方便对具有相同前缀的一组路由应用相同的中间件,可提高代码的可维护性和复用性。




8.4 后置中间件

在 Gin 框架里,中间件的执行顺序是按注册顺序来的,默认在业务处理函数之前执行。不过,你也能实现 “后置中间件”,让中间件在业务处理函数之后执行。下面为你介绍实现后置中间件的方法。


实现原理

Gin 框架里,c.Next() 方法会让当前中间件暂停,接着执行后续的中间件和业务处理函数,等后续逻辑执行完毕后,再回到当前中间件继续执行剩下的代码。利用这个特性,我们可以把中间件的主要逻辑放在 c.Next() 之后,这样就实现了后置中间件的效果。


示例代码

下面通过一个简单的例子,展示如何实现后置中间件,这个后置中间件会在业务处理函数执行完成后记录响应状态码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

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

// PostMiddleware 是一个后置中间件,会在业务处理函数执行后记录响应状态码
func PostMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 先调用 c.Next() 执行后续的中间件和业务处理函数
c.Next()
// 后续的中间件和业务处理函数执行完毕后,执行下面的代码
statusCode := c.Writer.Status()
log.Printf("Response status code: %d", statusCode)
}
}

func main() {
r := gin.Default()

// 注册后置中间件
r.Use(PostMiddleware())

r.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, World!",
})
})

r.Run(":8080")
}

代码解释

  1. 后置中间件定义
    • PostMiddleware 函数返回一个 gin.HandlerFunc 类型的函数。
    • 在返回的函数里,先调用 c.Next(),这会让当前中间件暂停,去执行后续的中间件和业务处理函数。
    • 等后续逻辑执行完毕后,再执行 c.Writer.Status() 获取响应状态码,并记录日志。
  2. 中间件注册
    • main 函数中,使用 r.Use(PostMiddleware()) 注册后置中间件。
  3. 业务处理函数
    • 定义 /test 路由的处理函数,返回一个 JSON 响应。




8.5 中间件注意事项

(一)、Gin 默认中间件

  • gin.Default()
    • 默认中间件:使用 gin.Default() 创建的路由默认应用了 LoggerRecovery 中间件。
    • Logger 中间件:无论是否配置 GIN_MODE=release,该中间件都会将日志写入 gin.DefaultWriter,方便开发者记录请求信息和跟踪应用运行状态。
    • Recovery 中间件:会捕获应用中出现的任何 panic,若发生 panic,会向客户端返回 500 响应码,避免因未处理的 panic 导致应用崩溃,增强了应用的健壮性。
  • gin.New():若不想使用上述默认中间件,可使用 gin.New() 创建一个没有任何默认中间件的路由实例,开发者可根据自身需求选择性地添加中间件。

(二)、Gin 中间件中使用 goroutine

  • 上下文使用限制:当在中间件或处理函数(handler)中启动新的 goroutine 时,不能直接使用原始的上下文 c *gin.Context。因为 gin.Context 不是并发安全的,多个 goroutine 同时访问和修改同一个上下文可能会导致数据竞争和不可预期的结果。(不是只读的)
  • 解决方案:必须使用原始上下文的只读副本 c.Copy()c.Copy() 会创建一个新的上下文副本,该副本包含了原始上下文的只读信息,可安全地在新的 goroutine 中使用。

(三)、示例代码说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

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

func asyncMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 创建上下文副本
copyContext := c.Copy()

go func() {
// 模拟耗时操作
time.Sleep(2 * time.Second)
// 使用上下文副本
log.Printf("Async operation finished for path: %s", copyContext.Request.URL.Path)
}()

c.Next()
}
}

func main() {
// 使用 gin.New() 创建无默认中间件的路由
r := gin.New()

// 添加自定义中间件
r.Use(asyncMiddleware())

r.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, World!",
})
})

r.Run(":8080")
}

在上述代码中:

  • 使用 gin.New() 创建了一个没有默认中间件的路由实例。
  • 定义了一个异步中间件 asyncMiddleware,在该中间件中启动了一个新的 goroutine
  • goroutine 中使用 c.Copy() 创建了上下文副本 copyContext,并使用该副本进行操作,避免了并发安全问题。

(四)、顺序问题

  • 顺序问题:中间件的执行顺序按照注册的顺序依次执行,因此需要合理安排中间件的注册顺序。
  • c.Next()c.Abort()c.Next() 用于继续执行后续的处理程序,c.Abort() 用于终止后续处理程序的执行。
  • 上下文数据共享:可以使用 c.Setc.Get 方法在请求上下文中设置和获取数据,方便不同中间件和处理函数之间共享数据。




九、实践

9.1 路由函数扩展(携带更多的参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package main

import (
"fmt"
"net/http"

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

func main() {
um := NewUserManager()

router := gin.Default()

user := router.Group("/user")
{
user.POST("/get_user_info", NewGetUserInfoHandler(um))
}

router.Run(":8080")
}

// -------------------------------------------
// -------------- | 协议部分 | ----------------
// -------------------------------------------

type UserInfo struct {
UserID uint64 `json:"user_id"`
UserName string `json:"user_name"`
Age uint8 `json:"age"`
Email string `json:"email"`
}

type GetUserInfoRequest struct {
UserID uint64 `json:"user_id"`
}

type GetUserInfoResponse struct {
UserInfo *UserInfo `json:"user_info"`
}

// -------------------------------------------------
// -------------- | 路由处理函数部分 | ----------------
// -------------------------------------------------

type GetUserInfoHandler struct {
request GetUserInfoRequest
response *GetUserInfoResponse
um UserManager
}

func NewGetUserInfoHandler(um UserManager) gin.HandlerFunc {
return func(c *gin.Context) {
getUserInfo := &GetUserInfoHandler{
um: um,
}
getUserInfo.Handle(c)
}
}

func (g *GetUserInfoHandler) Handle(c *gin.Context) {
if err := c.ShouldBind(&g.request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"msg": "GetUserInfo failed",
})
return
}

userInfo, err := g.um.GetUserInfoByUserID(g.request.UserID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"msg": "GetUserInfo failed",
})
return
}

g.response = &GetUserInfoResponse{
UserInfo: userInfo,
}
c.JSON(http.StatusOK, g.response)
}

// --------------------------------------------------
// -------------- | 数据访问层 mysql | ----------------
// --------------------------------------------------

type UserManager interface {
GetUserInfoByUserID(userID uint64) (*UserInfo, error)
}

func NewUserManager() UserManager {
return &MysqlClient{}
}

type MysqlClient struct {
}

func (m *MysqlClient) GetUserInfoByUserID(userID uint64) (*UserInfo, error) {
if userID != 666666 {
return nil, fmt.Errorf("userID: %d not exist", userID)
}
return &UserInfo{
UserID: 1,
UserName: "ggw",
Age: 18,
Email: "gavin.gong.it@gmail.com",
}, nil
}





9.2 自定义 panic recover 中间件

使用本站文章 go代码模块记录 中的代码来改造

package util
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package util

import "log"

type panicHandler func(err interface{})

type optionPanicHandler func(*optionPanicHandlerParams)

type optionPanicHandlerParams struct {
panicHandler panicHandler
}

func WithPanicHandler(panicHandler panicHandler) optionPanicHandler {
return func(optParams *optionPanicHandlerParams) {
optParams.panicHandler = panicHandler
}
}

func WithRecover(f func(), opts ...optionPanicHandler) {
defer func() {
if err := recover(); err != nil {
optParams := &optionPanicHandlerParams{}
for _, opt := range opts {
opt(optParams)
}
if optParams.panicHandler == nil {
optParams.panicHandler = defaultPanicHandler
}
optParams.panicHandler(err)
}
}()

f()
}

func defaultPanicHandler(err interface{}) {
log.Printf("panic: %v", err)
}
1
2
3
4
5
6
7
8
9
func PanicRecoverDefault(c *gin.Context) {
util.WithRecover(c.Next)
}

func PanicRecoverWithInfo(c *gin.Context) {
util.WithRecover(c.Next, util.WithPanicHandler(func(err interface{}){
// TODO: 记录日志
}))
}