Gin
一、下载与示例
下载并安装Gin
:
1 |
|
例子:
1 |
|
将上面的代码保存并编译执行,然后使用浏览器打开127.0.0.1:8080/hello
就能看到一串JSON字符串。
二、RESTful API
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 |
|
users/index.html
文件的内容如下:
1 |
|
Gin框架中使用LoadHTMLGlob()
或者LoadHTMLFiles()
方法进行HTML模板渲染。
1 |
|
函数解释
LoadHTMLGlob()
1 |
|
- 功能:
LoadHTMLGlob()
方法借助 Go 语言的filepath.Glob
函数,按照指定的模式来匹配文件,进而加载所有符合条件的 HTML 模板文件。 - 参数:该方法接收一个字符串参数,此参数为匹配文件的模式。在示例中,
"templates/**/*"
表示要加载templates
文件夹及其所有子文件夹内的全部文件。 - 优点:使用通配符能一次性加载多个模板文件,当项目中的模板文件较多时,可显著减少代码量。
- 缺点:若模板文件较多,加载时间可能会延长。
LoadHTMLFiles()
1 |
|
- 功能:
LoadHTMLFiles()
方法用于逐个加载指定的 HTML 模板文件。 - 参数:该方法接收多个字符串参数,每个参数代表一个模板文件的路径。在示例里,它会加载
templates/posts/index.html
和templates/users/index.html
这两个文件。 - 优点:可以精准地控制要加载的模板文件,仅加载必要的文件,能减少不必要的加载时间。
- 缺点:当模板文件数量较多时,需要列出每个文件的路径,代码会变得冗长。
3.2 自定义模版函数
服务器端代码
1 |
|
- 创建 Gin 引擎:
router := gin.Default()
创建了一个默认的 Gin 引擎实例,该实例包含了一些常用的中间件,如日志记录和恢复中间件。 - 定义自定义模板函数:
router.SetFuncMap
方法用于设置自定义的模板函数。在这个例子中,我们定义了一个名为safe
的函数,它接收一个字符串参数,并将其转换为template.HTML
类型。template.HTML
类型告诉 Go 的模板引擎,这个字符串不需要进行 HTML 转义。 - 加载模板文件:
router.LoadHTMLFiles("./index.tmpl")
加载了指定的模板文件index.tmpl
。 - 定义路由处理函数:
router.GET("/index", ...)
定义了一个处理/index
路径的 GET 请求的路由处理函数。在这个函数中,我们使用c.HTML
方法渲染index.tmpl
模板,并将一个 HTML 字符串作为数据传递给模板。 - 启动服务器:
router.Run(":8080")
启动了一个 HTTP 服务器,监听:8080
端口。
HTML 模板文件
1 |
|
在 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 |
|
对应的 index.tmpl
模板文件:
1 |
|
在这个模板里,{{.title}}
会被替换成 "users/index"
。
.
当传递的是单个值(像字符串、整数等)时,在模板中使用 .
就可以引用这个值。比如,当使用 c.HTML
传递 "<a href='https://gxblogs.com'>GXblogs</a>"
时,在模板里使用 .
就能获取到这个字符串。
1 |
|
对应的 index.tmpl
模板文件:
1 |
|
在这个模板里,{{.}}
会被替换成 "<a href='https://gxblogs.com'>GXblogs</a>"
。
综上所述,在模板中 .
代表传递的数据对象,若传递的是 map
,就可以用 .键名
引用具体的值;若传递的是单个值,直接用 .
引用该值。
ps: 在之前的代码示例里,可能没看到 safe
函数起作用,下面详细分析并给出能体现其作用的示例。
safe
函数的主要作用是告诉 Go 的模板引擎,传入的字符串不需要进行 HTML 转义,可直接当作 HTML 代码渲染。但如果没有正确使用该函数,或者传入的数据已经被转义过,就可能看不到其效果。
未使用自定义转义函数的效果:
3.4 自定义分隔符
1 |
|
在 Gin 框架中,Delims
方法允许你自定义模板引擎所使用的分隔符。默认情况下,Go 语言的模板引擎使用 {{` 和 `}}
作为分隔符,不过借助 Delims
方法,你能够把它们替换成自定义的分隔符,例如 {[ {
和 } ]}
。
下面是一个完整的示例,展示了怎样使用自定义分隔符:
1 |
|
1 |
|
代码解释
- 自定义分隔符:在
main
函数里,调用router.Delims("{[{", "}]}")
把模板引擎的分隔符设置成{[ {
和} ]}
。 - 加载模板文件:使用
router.LoadHTMLGlob("templates/**/*")
加载templates
目录下的所有模板文件。 - 定义自定义模板函数:和之前一样,定义了
safe
函数用于防止 HTML 转义。 - 路由处理函数:处理
/
路径的 GET 请求,将包含 HTML 标签的字符串传递给模板。 - 模板文件:在
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/template
和html/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 |
|
header.html
模板文件内容
1 |
|
footer.html
模板文件内容
1 |
|
userInfo.html
模板文件内容
1 |
|
postDetail.html
模板文件内容
1 |
|
Gin 服务器端代码
1 |
|
在这个示例中,通过 {{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 |
|
base.tmpl
:作为基础模板,放置所有页面共有的 HTML 结构、CSS 样式链接、JavaScript 脚本链接等内容。例如,包含页面的整体布局、通用的头部导航栏和底部版权信息。specificPage1.tmpl
和specificPage2.tmpl
:这些是具体页面的模板,继承自base.tmpl
,主要编写每个页面独特的内容部分。otherTemplates.tmpl
:可用于存放其他通用的模板片段,供基础模板或具体页面模板复用。
(三)编写模板文件
- 基础模板(base.tmpl)
使用{{block}}
指令定义可被覆盖的区域。例如:
1 |
|
{{block "title" .}}默认标题{{end}}
定义了title
块,子模板可覆盖该块来设置特定页面标题。{{block "content" .}}...{{end}}
定义了主要内容区域,子模板可在此处填充各自的内容。
- 子模板(以 specificPage1.tmpl 为例)
使用{{define}}
指令来覆盖基础模板中的{{block}}
区域,并通过{{template}}
指令引用基础模板。例如:
1 |
|
{{define "specificPage1.tmpl"}}
定义了该模板的名称。{{template "base.tmpl" .}}
表示继承base.tmpl
。通过重新定义title
和content
块,实现对基础模板相应区域的定制。
(四)在 Go 代码中加载和使用模板
- 定义加载模板的函数
1 |
|
multitemplate.NewRenderer()
:创建一个新的multitemplate.Renderer
实例,用于管理多个模板。filepath.Glob
:获取指定目录下的所有.tmpl
文件。分别获取layouts
目录(存放基础模板)和includes
目录(存放子模板)下的所有模板文件。- 循环遍历
includes
目录下的每个子模板文件:- 复制
layouts
切片,避免后续操作修改原始切片。 - 将当前子模板文件添加到复制的
layouts
切片中,形成一个包含基础模板和当前子模板的新切片files
。 - 使用
r.AddFromFiles(filepath.Base(include), files...)
将这些文件添加到Renderer
中,其中filepath.Base(include)
作为模板名称,files...
是该模板包含的所有文件。
- 复制
- 在 main 函数中配置和使用模板
1 |
|
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
端口。
三、注意事项
- 模板名称的一致性:在
loadTemplates
函数中通过filepath.Base(include)
设置的模板名称,要与在c.HTML
方法中使用的模板名称完全一致,否则无法正确渲染模板。 - 块定义与覆盖的顺序:在子模板中,覆盖基础模板的
{{block}}
区域的{{define}}
指令应在{{template}}
指令之后,且{{define}}
指令内的块名称要与基础模板中的{{block}}
名称一致。 - 数据传递:在渲染模板时(如
c.HTML
方法)传递的数据,可以在基础模板和子模板中使用 Go 模板语法进行访问和展示。子模板继承基础模板的数据访问规则,且可以在其覆盖的块中根据需要进一步处理和展示数据。
评价模版继承
在上述代码中,一个路由引擎确实只有一个模板基类(base.tmpl
),这种设计有其合理性和局限性,具体分析如下:
合理性
- 统一页面布局:在大多数网页应用中,通常希望多个页面具有统一的布局,如相同的导航栏、页脚等。通过使用单一的模板基类,可以方便地在一个地方定义这些公共部分,确保所有继承该基类的页面具有一致的外观和风格,有利于保持网站的整体一致性。
- 易于维护:当需要对网站的整体布局进行修改时,只需在基类模板中进行更改,所有继承该基类的页面都会自动应用这些修改,无需逐个修改每个页面的相关代码,大大提高了维护效率。
局限性
- 缺乏灵活性:如果有一些特殊页面需要完全不同的布局,使用单一基类可能会受到限制。因为所有页面都继承自同一个基类,要实现特殊布局可能需要额外的处理或对基类进行复杂的条件判断,这可能会增加代码的复杂性。
- 功能扩展受限:随着项目的发展,如果需要引入新的布局风格或对不同类型的页面进行更细致的布局管理,单一基类可能无法很好地满足需求。可能需要对代码结构进行较大的调整才能实现新的布局要求。
如果项目中存在多种不同类型的页面,且它们的布局差异较大,可能需要考虑更灵活的模板设计方案,例如使用多个基类模板,或者根据不同的路由或业务需求动态选择合适的基类模板来渲染页面。但对于一些布局相对统一的小型项目或特定场景,使用一个模板基类可以有效地提高开发效率和代码的可维护性。
3.8 静态文件
在使用 Gin 框架构建 Web 应用时,处理静态文件(如 CSS、JavaScript、图片等)是很常见的需求。下面为你详细解释这段代码中静态文件处理的部分,以及在 HTML 文件中如何引用这些静态文件。
代码解释
1 |
|
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 |
|
在 templates/index.html
文件中,你可以按照以下方式引用静态文件:
1 |
|
在上述 HTML 代码中,通过 /static
前缀来引用静态文件,Gin 会根据之前设置的静态文件映射规则,从对应的本地目录中查找并返回这些文件。
注意事项
- 确保
./static
目录存在,并且包含你需要提供的静态文件。 - 静态文件的路径是相对于项目根目录的,因此要注意文件的实际位置。
- 如果需要修改静态文件的访问路径或本地目录,只需修改
r.Static
方法的参数即可。
3.9 返回json格式数据
1 |
|
- 作用:通过 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 |
|
c.DefaultQuery(key, defaultValue string) string
:用于获取指定名称的查询参数,若参数不存在则返回默认值。c.Query(key string) string
:用于获取指定名称的查询参数,若参数不存在则返回空字符串。
4.2 获取form参数
当前端请求的数据通过form表单提交时,例如向/user/search
发送一个POST请求,获取请求数据的方式如下:
1 |
|
c.PostForm(key string) string
:用于获取表单中指定名称的参数值,若参数不存在则返回空字符串。c.DefaultPostForm(key, defaultValue string) string
:同样用于获取表单参数,若参数不存在则返回指定的默认值。
4.3 获取JSON参数
当前端请求的数据通过JSON提交时,例如向/json
发送一个JSON格式的POST请求,则获取请求参数的方式如下:
1 |
|
-
读取原始数据:
c.GetRawData()
从c.Request.Body
中读取请求携带的原始数据,返回的是字节切片[]byte
。这里为了简化示例,忽略了错误处理。 -
定义存储结构:
var m map[string]interface{}
定义了一个map
,键为string
类型,值为interface{}
类型,用于存储反序列化后的 JSON 数据。也可以使用结构体来存储,但需要提前定义好结构体的字段和类型。
4.4 获取path参数
请求的参数通过URL路径传递,例如:/user/search/小王子/沙河
。 获取请求URL路径中的参数的方式如下。
1 |
|
-
定义路由:
r.GET("/user/search/:username/:address", ...)
定义了一个 GET 请求的路由,其中:username
和:address
是动态路径参数。当客户端发送符合该路径格式的请求时,会执行对应的处理函数。 -
获取路径参数:
c.Param("username")
和c.Param("address")
用于从 URL 路径中提取username
和address
参数的值。
4.5 参数绑定(反射)
在 Go 的 Gin 框架开发中,为高效获取请求相关参数,可利用反射机制根据请求的 Content-Type
识别数据类型,将 QueryString、form 表单、JSON、XML 等参数自动提取到结构体中。
示例代码
1 |
|
代码关键部分解释
-
结构体定义:定义
Login
结构体,通过标签form
、json
指定在不同数据格式下的字段名,binding:"required"
表示该字段为必需项。 -
路由处理:
/loginJSON
:处理 POST 请求,绑定 JSON 数据。若绑定成功,返回用户信息;若失败,返回错误信息。/loginForm
(POST):处理 POST 请求,绑定 form 表单数据。绑定逻辑与/loginJSON
类似。/loginForm
(GET):处理 GET 请求,绑定 QueryString 数据。同样根据绑定结果返回相应信息。
-
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 结构体的字段上,让开发者能更便捷地处理请求参数。
工作原理
- 解析查询字符串:当接收到 GET 请求时,Form 绑定引擎会从 URL 中提取查询字符串,然后把它拆分成一个个键值对。
- 结构体标签匹配:在 Go 结构体中,开发者可以使用
form
标签来指定每个字段对应的查询字符串参数名。Form 绑定引擎会依据这些标签,将解析出的键值对与结构体字段进行匹配。 - 数据类型转换:引擎会尝试把查询字符串中的值转换为结构体字段对应的数据类型。例如,若结构体字段是
int
类型,引擎会把查询字符串中的值转换为整数。 - 绑定到结构体:完成匹配和类型转换后,引擎会把解析和转换后的值赋给结构体的相应字段。
使用示例
1 |
|
在这个示例中:
- 定义了
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-urlencoded 或 multipart/form-data ) |
将表单数据(包括 application/x-www-form-urlencoded 和 multipart/form-data 格式)绑定到结构体,绑定失败返回错误给调用者处理。 |
BindForm |
Form(application/x-www-form-urlencoded 或 multipart/form-data ) |
将表单数据(包括 application/x-www-form-urlencoded 和 multipart/form-data 格式)绑定到结构体,绑定失败直接向客户端返回 400 Bad Request 响应。 |
用带should的就行,类似上面的处理。
五、文件上传
这里介绍了使用 Go 的 Gin 框架实现文件上传功能,包含单个文件上传和多个文件上传的具体实现,同时给出了对应的前端页面代码。
前端代码
前端通过 HTML 表单实现文件选择与上传功能,关键要点如下:
- 表单设置:使用
POST
方法和enctype="multipart/form-data"
,这是文件上传必需的配置。 - 文件选择:通过
<input type="file" name="f1">
让用户选择要上传的文件。 - 提交按钮:
<input type="submit" value="上传">
用于提交表单。
1 |
|
后端代码
(一)单个文件上传
1 |
|
- 路由设置:使用
router.POST("/upload", ...)
处理文件上传请求。 - 获取文件:通过
c.FormFile("f1")
获取上传的单个文件,若出现错误则返回500
状态码和错误信息。 - 保存文件:使用
c.SaveUploadedFile(file, dst)
将文件保存到指定目录(这里是C:/tmp
)。 - 返回响应:上传成功后返回
200
状态码和上传成功的消息。 - 内存限制:可通过
router.MaxMultipartMemory
修改处理multipart forms
提交文件时的内存限制。
(二)多个文件上传
1 |
|
- 路由设置:同样使用
router.POST("/upload", ...)
处理文件上传请求。 - 获取文件列表:通过
c.MultipartForm()
获取表单数据,从中提取所有上传的文件(form.File["file"]
)。 - 循环保存文件:遍历文件列表,使用
c.SaveUploadedFile(file, dst)
将每个文件保存到指定目录,为避免文件名冲突,在文件名后添加索引。 - 返回响应:上传成功后返回
200
状态码和上传文件数量的消息。 - 内存限制:同单个文件上传,可修改内存限制。
注意事项
- 错误处理:实际开发中要完善错误处理逻辑,涵盖获取文件和保存文件时可能出现的各类错误。
- 文件命名:多个文件上传时,要考虑文件名冲突问题,可采用添加索引等方式解决。
- 内存管理:根据实际情况调整
router.MaxMultipartMemory
的值,防止因内存不足导致上传失败。 - 文件路径权限:确保服务器有足够权限在指定路径下创建和写入文件。
六、重定向
6.1 HTTP 重定向
- 特点
支持内部和外部重定向,使用简单。
- 示例代码
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 路由重定向
- 特点
通过修改请求的 URL 路径,然后使用 HandleContext
方法将请求交给另一个路由处理,实现路由间的重定向。
- 示例代码
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 |
|
- POST 请求路由:常用于向服务器提交数据,如表单提交等操作。
1 |
|
(二)、Any 方法路由
Any
方法可以匹配所有的 HTTP 请求方法,包括 GET、POST、PUT、DELETE 等。当你希望某个路由对所有请求方法都进行相同的处理时,可使用此方法。
1 |
|
(三)、NoRoute 路由
NoRoute
方法用于为没有配置处理函数的路由添加处理程序。当客户端请求的路由在现有路由配置中未找到匹配时,会执行 NoRoute
中定义的处理逻辑。默认情况下,Gin 对未匹配的路由返回 404 状态码,可通过自定义 NoRoute
处理函数来返回特定的页面或信息。
1 |
|
上述代码中,当请求的路由未匹配到时,会返回 views/404.html
页面给客户端,状态码为 404。
(四)、注意事项
- 路由顺序:Gin 按路由定义的顺序匹配请求,因此要合理安排路由顺序,避免规则冲突。
- 错误处理:在实际应用中,可在路由处理函数中添加错误处理逻辑,增强程序健壮性。
- 路径参数:Gin 支持在路由路径中使用参数,可进一步扩展路由功能,实现动态路由。
7.2 路由组
(一)、路由组概念
在 Gin 框架里,可把拥有相同 URL 前缀的路由归为一个路由组。使用路由组能让代码结构更清晰,便于管理和维护不同业务逻辑或不同版本的 API。
(二)、基本路由组使用
通过 r.Group(prefix)
方法创建路由组,prefix
为该路由组的 URL 前缀,组内的路由路径会自动添加此前缀。
1 |
|
userGroup
:URL 前缀为/user
,组内路由/index
实际访问路径是/user/index
,/login
实际路径是/user/login
。shopGroup
:URL 前缀为/shop
,组内路由/index
实际路径是/shop/index
,/cart
实际路径是/shop/cart
等。- 花括号作用:使用
{}
包裹同组路由主要是为了增强代码可读性,对功能无影响。
(三)、路由组嵌套
路由组支持嵌套,可进一步细化路由结构。
1 |
|
shopGroup
:URL 前缀为/shop
。xx
嵌套路由组:它是shopGroup
的子组,URL 前缀为/shop/xx
,组内路由/oo
实际访问路径是/shop/xx/oo
。
(四)、应用场景
- 业务逻辑划分:如示例中的
user
路由组处理用户相关业务,shop
路由组处理商城相关业务,让代码按业务模块组织,提高可维护性。 - API 版本划分:在开发不同版本的 API 时,可按版本号创建路由组,如
/v1
、/v2
等,方便管理和升级 API。
7.3 路由原理
(一)、基于 httprouter
库的路由原理
Gin 框架采用 httprouter
库来实现路由功能,其核心原理是构建一个路由地址的前缀树(也叫前缀树或字典树)结构。
- 前缀树构建:当定义路由时,比如有
/user
、/user/profile
、/user/settings
这些路由,httprouter
会将它们按照路径的前缀关系组织成一棵前缀树。树的节点代表路径中的一部分,从根节点到某个叶子节点的路径就对应一个完整的路由。例如,根节点可能代表根路径,/user
是根节点的一个子节点,/user/profile
和/user/settings
则是/user
节点的子节点。 - 路由匹配:当客户端发起请求时,
httprouter
会根据请求的 URL 路径在这棵前缀树上进行查找匹配。从根节点开始,依次比较路径的各个部分,找到匹配的节点。如果找到了完全匹配的叶子节点,就执行对应的路由处理函数;如果没有找到完全匹配的节点,但存在部分匹配的节点,也可能根据情况进行处理(比如返回 404 或其他错误)。 - 参数处理:对于包含参数的路由,如
/user/:id
,httprouter
会在匹配过程中识别出参数部分,并将参数值提取出来,传递给路由处理函数。它通过在路径中使用特定的标记(如:
后面跟着参数名)来区分参数和普通路径部分。
(二)、httprouter
库实现路由的优点
- 高效性:前缀树结构使得路由匹配的时间复杂度较低,通常为 O (n),其中 n 是路径的长度。这意味着在处理大量路由时,能够快速地找到匹配的路由,提高了路由匹配的性能。
- 灵活性:支持多种类型的路由,包括静态路由、带参数的路由等,能够满足不同应用场景的需求。同时,对于参数的处理也比较灵活,可以方便地在路由处理函数中获取参数值。
- 内存占用相对较小:相比于一些其他的路由实现方式,前缀树结构在存储路由信息时,能够有效地减少内存的占用,因为它共享了路径的前缀部分。
7.4 带参数的路由
在 Gin 框架里,带参数的路由能让你定义动态的路由规则,可处理包含可变部分的 URL,以下为你详细介绍带参数的路由以及参数规则设置方法:
带参数的路由类型及示例
- 命名参数
- 规则:使用
:
来定义命名参数,在路由处理函数里可以通过c.Param
方法获取参数值。 - 示例代码
1 |
|
- 解释:当客户端请求
/user/john
时,name
参数的值为john
,处理函数会返回{"message": "Hello, john"}
。
- 通配符参数
- 规则:使用
*
来定义通配符参数,它可以匹配路径中剩余的任意部分。通配符参数必须是路由路径的最后一部分。 - 示例代码
1 |
|
- 解释:当客户端请求
/files/documents/report.pdf
时,filepath
参数的值为/documents/report.pdf
,处理函数会返回{"message": "File path: /documents/report.pdf"}
。
设置参数规则
- 正则表达式约束(借助中间件)
Gin 本身未直接支持正则表达式来约束参数,但可以通过自定义中间件实现。
1 |
|
- 解释:上述代码定义了一个
validateID
中间件,使用正则表达式^[0-9]+$
验证id
参数是否为纯数字。若不符合规则,返回400 Bad Request
错误响应。
- 手动验证
在路由处理函数中手动验证参数的合法性。
1 |
|
- 解释:在这个例子中,在路由处理函数里手动将
id
参数转换为整数,若转换失败则返回错误响应。
八、中间件
Gin 框架支持开发者在处理请求的流程中插入自定义的钩子函数,这些钩子函数被称为中间件。中间件适合处理公共业务逻辑,如登录认证、权限校验、数据分页、日志记录、耗时统计等。
8.1 中间件定义规则
Gin 中的中间件必须是 gin.HandlerFunc
类型,该类型是一个接收 *gin.Context
参数且无返回值的函数。
1 |
|
8.2 常见中间件示例
> 记录接口耗时的中间件
1 |
|
- 功能:统计每个请求的处理耗时并记录日志。
- 实现步骤:
- 在中间件函数开始处记录当前时间。
- 使用
c.Set
方法在请求上下文中设置值,后续的处理函数可以通过c.Get
方法获取该值。 - 调用
c.Next()
方法继续执行后续的处理程序。 - 计算从开始到当前的时间差,即请求处理耗时,并记录日志。
- 若调用
c.Abort()
方法,则会终止后续处理程序的执行。
> 记录响应体的中间件
1 |
|
- 功能:记录返回给客户端的响应体数据。
- 实现步骤:
- 定义一个自定义的
bodyLogWriter
结构体,嵌入gin.ResponseWriter
并添加一个bytes.Buffer
用于记录响应体数据。 - 实现
Write
方法,在该方法中先将响应体数据写入bytes.Buffer
进行记录,再调用原始的Write
方法将数据写入响应。 - 在中间件函数中,创建
bodyLogWriter
实例并替换默认的c.Writer
。 - 调用
c.Next()
方法执行后续的处理程序。 - 处理程序执行完毕后,从
bytes.Buffer
中获取记录的响应体数据并记录日志。
- 定义一个自定义的
> 跨域中间件cors
跨域问题与解决方案
在前后端分离架构中,由于浏览器的同源策略,会出现跨域问题。为解决该问题,推荐使用社区的 github.com/gin-contrib/cors
库,该库能通过简单配置解决跨域问题,且中间件需注册在业务处理函数之前。
详细配置使用
1 |
|
配置项说明:
AllowOrigins
:指定允许跨域请求的源站列表。AllowMethods
:定义允许的请求方法,如GET
、POST
等。AllowHeaders
:设置允许的请求头。ExposeHeaders
:指定可以暴露给客户端的响应头。AllowCredentials
:若设为true
,则允许在跨域请求中携带凭证(如 cookie)。AllowOriginFunc
:自定义源站过滤函数,根据传入的源站字符串返回布尔值来决定是否允许该源站的请求。MaxAge
:预检请求(Preflight Request)的缓存时间,减少不必要的预检请求。
默认配置使用
1 |
|
- 功能:使用默认配置,允许所有的跨域请求。这种方式简单直接,但在生产环境中可能存在安全风险,需谨慎使用。若需要更精细的跨域控制,建议使用详细配置的方式。
注意事项
- 中间件注册顺序:跨域中间件必须注册在业务处理函数之前,以确保在处理业务逻辑之前先处理跨域相关的问题。
- 安全考量:在使用
AllowAllOrigins
或自定义AllowOriginFunc
时,要充分考虑安全问题,避免开放过多不必要的跨域权限。
8.3 注册中间件
在 Gin 框架里,能为每个路由添加任意数量的中间件,可根据不同需求为全局路由、单个路由或路由组注册中间件。
(一)、为全局路由注册中间件
1 |
|
步骤:
- 使用
gin.New()
创建一个没有默认中间件的路由实例。 - 运用
r.Use(StatCost())
注册全局中间件StatCost()
,这样所有路由都会经过该中间件处理。 - 定义具体路由,在处理函数中可通过
c.MustGet
从上下文获取中间件设置的值。
(二)、为某个路由单独注册中间件
1 |
|
特点:仅对 /test2
这个特定路由应用 StatCost()
中间件。若需要多个中间件,可依次罗列在路由处理函数之前,中间件会按顺序执行。
(三)、为路由组注册中间件
法一:
1 |
|
- 步骤:在创建路由组
shopGroup
时,将中间件StatCost()
作为参数传入,该路由组下的所有路由都会使用此中间件。
法二:
1 |
|
- 步骤:先创建路由组
shopGroup
,再使用shopGroup.Use(StatCost())
为该路由组注册中间件,效果与写法 1 相同。
总结
- 全局注册:适用于需要对所有请求进行统一处理的场景,如日志记录、请求耗时统计等。
- 单个路由注册:用于仅对特定路由添加特殊处理逻辑的情况。
- 路由组注册:方便对具有相同前缀的一组路由应用相同的中间件,可提高代码的可维护性和复用性。
8.4 后置中间件
在 Gin 框架里,中间件的执行顺序是按注册顺序来的,默认在业务处理函数之前执行。不过,你也能实现 “后置中间件”,让中间件在业务处理函数之后执行。下面为你介绍实现后置中间件的方法。
实现原理
Gin 框架里,c.Next()
方法会让当前中间件暂停,接着执行后续的中间件和业务处理函数,等后续逻辑执行完毕后,再回到当前中间件继续执行剩下的代码。利用这个特性,我们可以把中间件的主要逻辑放在 c.Next()
之后,这样就实现了后置中间件的效果。
示例代码
下面通过一个简单的例子,展示如何实现后置中间件,这个后置中间件会在业务处理函数执行完成后记录响应状态码。
1 |
|
代码解释
- 后置中间件定义:
PostMiddleware
函数返回一个gin.HandlerFunc
类型的函数。- 在返回的函数里,先调用
c.Next()
,这会让当前中间件暂停,去执行后续的中间件和业务处理函数。 - 等后续逻辑执行完毕后,再执行
c.Writer.Status()
获取响应状态码,并记录日志。
- 中间件注册:
- 在
main
函数中,使用r.Use(PostMiddleware())
注册后置中间件。
- 在
- 业务处理函数:
- 定义
/test
路由的处理函数,返回一个 JSON 响应。
- 定义
8.5 中间件注意事项
(一)、Gin 默认中间件
gin.Default()
- 默认中间件:使用
gin.Default()
创建的路由默认应用了Logger
和Recovery
中间件。 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 |
|
在上述代码中:
- 使用
gin.New()
创建了一个没有默认中间件的路由实例。 - 定义了一个异步中间件
asyncMiddleware
,在该中间件中启动了一个新的goroutine
。 - 在
goroutine
中使用c.Copy()
创建了上下文副本copyContext
,并使用该副本进行操作,避免了并发安全问题。
(四)、顺序问题
- 顺序问题:中间件的执行顺序按照注册的顺序依次执行,因此需要合理安排中间件的注册顺序。
c.Next()
和c.Abort()
:c.Next()
用于继续执行后续的处理程序,c.Abort()
用于终止后续处理程序的执行。- 上下文数据共享:可以使用
c.Set
和c.Get
方法在请求上下文中设置和获取数据,方便不同中间件和处理函数之间共享数据。
九、实践
9.1 路由函数扩展(携带更多的参数)
1 |
|
9.2 自定义 panic recover 中间件
使用本站文章 go代码模块记录 中的代码来改造
package util
1 |
|
1 |
|