gin基础用法
目录常用结构图
├── main.go:应用程序入口文件
├── controllers:控制器目录,用于处理请求
│ ├── home_controller.go:处理首页请求的控制器
│ ├── user_controller.go:处理用户请求的控制器
│ └── …
├── models:模型目录,用于封装数据操作
│ ├── user.go:用户模型,用于操作用户数据
│ └── …
├── routes:路由目录,用于配置路由
│ ├── home_routes.go:配置首页路由
│ ├── user_routes.go:配置用户路由
│ └── …
├── services:服务目录,用于封装业务逻辑
│ ├── user_service.go:用户服务,用于处理用户相关业务逻辑
│ └── …
├── middlewares:中间件目录,用于处理请求前后的逻辑
│ ├── auth.go:身份认证中间件,用于检查用户是否登录
│ ├── logger.go:日志中间件,用于记录请求日志
│ └── …
├── static:静态文件目录,用于存放静态文件,如图片、CSS、JS等
│ ├── css:CSS文件目录
│ ├── js:JS文件目录
│ ├── img:图片文件目录
│ └── …
├── templates:模板目录,用于存放HTML模板文件
│ ├── home.html:首页模板文件
│ ├── user.html:用户模板文件
│ └── …
├── conf:配置文件目录,用于存放应用程序的配置文件
│ ├── app.conf:应用程序的基本配置文件,如端口号、数据库连接等
│ └── …
└── vendor:依赖包目录,用于存放应用程序的依赖包
快速入门
参考: https://gin-gonic.com/zh-cn/docs/quickstart/
要求
Go 1.13 及以上版本
安装
要安装 Gin 软件包,需要先安装 Go 并设置 Go 工作区。
1.下载并安装 gin:
go get -u github.com/gin-gonic/gin
2.将 gin 引入到代码中:
import "github.com/gin-gonic/gin"
3.(可选)如果使用诸如 http.StatusOK 之类的常量,则需要引入 net/http 包:
import "net/http"
开始
首先,创建一个名为 example.go 的文件
touch example.go
接下来, 将如下的代码写入 example.go 中:
1 | package main |
然后, 执行 go run example.go 命令来运行代码:
1 | # 运行 example.go 并且在浏览器中访问 HOST_IP:8080/ping |
注意:
- 使用gin.Default()创建路由时, 已经默认引入了Logger(), Recovery() 这两个中间件, 不需要重复设置。 这两个中间件的功能分别是日志和错误抛出。
- Logger 中间件将日志写入gin.DefaultWriter, 即使配置了 GIN_MODE=release。
- Recovery中间件会recover任何panic。如果有panic的话, 会写入500响应码。 也就是说,它可以防止程序因panic问题崩溃, 并且会在发生panic问题时返回500的响应码
在 gin 之外, 我们也常在goroutine中, 使用一个recover()函数来捕获异常。
此场景下, recover函数的作用:
- 使用匿名函数捕获test抛出的panic
- 是一个内建的函数,可以让进入令人恐慌的流程中的 goroutine 恢复过来。
- recover仅在延迟函数中有效。
- 在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果。
- 如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。recover只有在defer调⽤的函数中有效。
- 如果不想要默认中间件, 可以使用gin.New()来创建路由。
使用
可以返回的请求
- ctx.JSON()
可以是
结构体
或map键值对
, gin会自动将其转换为json数据进行传输。(记得,如果使用结构体,记得利用tag标签注释来设置变量名的小写- key为json
,value为变量名小写
, 多个tag用空格隔开) - ctx.JSONP()
可以是
结构体
或map键值对
, gin会自动将其转换为json数据进行传输。(记得,如果使用结构体,记得利用tag标签注释来设置变量名的小写- key为json
,value为变量名小写
, 多个tag用空格隔开)- jsonp的api在前端请求时, 是可以有回调方法的。比如,可以利用jsonp来解决跨域问题
- ctx.XML()
可以是
结构体
或map键值对
, gin会自动将其转换为json数据进行传输。(记得,如果使用结构体,记得利用tag标签注释来设置变量名的小写- key为xml
,value为变量名小写
, 多个tag用空格隔开) - ctx.String()
获取前端的传值
获取GET的传值
- ctx.Query()
- ctx.DefaultQuery() ->
带默认值, 可以设置前端为传递该值时,对应变量的默认值
获取post的传值
- ctx.PostForm()
- ctx.DefaultPostForm() ->
带默认值, 可以设置前端为传递该值时,对应变量的默认值
- ctx.ShouldBind()
注意:
第一点:
在Gin框架中,PostForm()
方法用于获取表单数据。然而,如果你使用axios发送请求,并且请求的Content-Type
被设置为application/json
,那么你可能无法使用PostForm()
方法获取数据,因为PostForm()
方法只能获取Content-Type
为application/x-www-form-urlencoded
或multipart/form-data
的数据。如果你的请求的
Content-Type
是application/json
,你应该使用Bind()
或者ShouldBind()
方法来获取请求体中的JSON数据。例如:1
2
3
4
5
6
7
8
9
10
11
12
13type MyData struct {
Field1 string `json:"field1"`
Field2 string `json:"field2"`
}
func MyHandler(c *gin.Context) {
var data MyData
if err := c.ShouldBindJSON(&data); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 现在你可以使用data.Field1和data.Field2来访问你的数据
}在这个例子中,如果你发送一个包含
field1
和field2
字段的JSON对象到这个处理器,那么这些字段的值将被绑定到data
变量中。而
前端axios对于Post请求, 只能使用json
, 因此用PostForm无法正常获取, 只能通过这种绑定结构体的方式获取。注意:
第二点:
在 Gin 框架中,ShouldBind
方法在第一次调用后,如果再次尝试读取request body
的数据,会出现EOF
的错误,因为c.Request.Body
不可以重用。这是因为在ShouldBind
方法调用过一次之后,context.request.body.sawEOF
的值是false
。如果你需要多次绑定多个变量,你需要使用
ShouldBindBodyWith
方法。ShouldBindBodyWith
方法可以支持重复绑定,原理就是将body
的数据缓存了下来。但是在二次取数据的时候还是得用ShouldBindBodyWith
才行,直接用ShouldBind
还是会报错的。ShouldBindBodyWith
方法被用来多次绑定request body
到不同的结构体中。请注意,你需要根据你的需求来选择合适的绑定方法。如果你只需要绑定一次,ShouldBind
或ShouldBindJSON
等方法可能更简单。如果你需要多次绑定,那么ShouldBindBodyWith
将是一个更好的选择。总结
- https://pkg.go.dev/github.com/gin-gonic/gin@v1.9.1#Context.ShouldBind
- 如果你需要更好的性能, 则
ShouldBindWith
系列更有性价比, 但它只能使用一次。 (包括ShouldBind
、ShouldBindJSON
等省略第二个参数的简化使用版) - 如果你需要多次绑定使用, 那么
ShouldBindBodyWith
与ShouldBindWith
类似,但它将请求正文存储到上下文中,并在再次调用时重用。(仅此一个,无简化使用版)
获取的 GET POST 值可以直接绑定到结构体上
- 利用tag标签注释来设置结构体内变量名的小写- key为
form
,value为变量名小写
, 多个tag用空格隔开) - 使用ctx.ShouldBind()
- 利用tag标签注释来设置结构体内变量名的小写- key为
获取 POST XML 数据
- 在结构体标签(tag)的key的其中一个, 使用xml来指定, value用变量名的小写。
- 使用ctx.GetRawData(), 得到的数据是xml的字节切片。
- 使用xml.Unmarshal(), 将字节切片转换至对应结构体。
动态路由参数传值
比较美观, 可以在没有
?
的条件下传值1
2
3
4r.GET("/xxx/:uid", func(ctx *gin.Context){
uid := ctx.Param("uid")
c.String(200, "uid=%s", uid)
})下方这个代码片段, 参考自https://gin-gonic.com/zh-cn/docs/examples/param-in-path/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20func main() {
router := gin.Default()
// 此 handler 将匹配 /user/john 但不会匹配 /user/ 或者 /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
// 此 handler 将匹配 /user/john/ 和 /user/john/send
// 如果没有其他路由匹配 /user/john,它将重定向到 /user/john/
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
router.Run(":8080")
}
路由分组
参考自https://gin-gonic.com/zh-cn/docs/examples/grouping-routes/
1 | func main() { |
当然, 我们也可以将某个路由分组, 从主文件中剥离至新的 package
所代表的一系列文件中。
1 | // 某个router分组的文件 |
然后在主文件中引入即可
1 | // main文件 |
对路由的业务方法进行剥离(控制器抽离)
https://www.bilibili.com/video/BV1fA411F7aM?p=7&spm_id_from=pageDriver
跳转 3min 时刻, 查看
中间件
通俗来将, 这里的中间件指的就是, 匹配路由之前, 以及匹配路由之后,执行的一系列操作。
路由中间件
每个路由api的最后一个方法作为业务方法, 之前的统统作为路由中间间方法, 在业务方法之前执行。
在路由中间件方法中,可以使用 ctx.Next()
方法, 来使路由中间件中此方法内 ctx.Next()
后的逻辑, 放到最后再执行。
ctx.Abort()
, 可以使此中间件外, 之后的其他中间件不再执行; 但此中间件方法内 ctx.Abort
之后的逻辑依然会继续执行完毕。
ctx.Next()
, 在多个中间件顺序使用时, 可以起到嵌套的作用。
全局中间件
路由使用前, 可在router.Use()
的形参内, 配置全局中间件。
全局中间件可作用于所有路由。
路由分组中间件
两种方法
- 直接在
router.Group()
的形参中配置路由分组中间件
(推荐使用这种方式) - 在
router.Group()
之后, 并且组内路由配置之前,使用routerGroup.Use()
来配置路由分组中间件
对中间件分组中的中间件方法
进行剥离()
https://www.bilibili.com/video/BV1fA411F7aM?p=8
跳转 22min 时刻, 查看
中间件方法
和路由业务方法(控制器handler)
之间如何共享数据
- 使用
ctx.Set()
来设置值 - 使用
ctx.Get()
来获取值- 由于此函数返回值是一个空接口类型, 所以使用前需要进行类型断言
变量.(具体类型)
- 由于此函数返回值是一个空接口类型, 所以使用前需要进行类型断言
在中间件方法
或路由业务方法(控制器handler)
使用goroutine
若在中间件方法
或路由业务方法(控制器handler)
使用goroutine的话, 不能使用原始上下文(ctx *gin.Context
), 必须使用其只读副本(c.Copy()
)
gin框架的使用中, 常会在输出日志等一些场景中, 会用到goroutine来并发进行日志的输出记录。
1 | func main() { |
自定义模板函数(自定义Model) - 用于html中的方法
自定义模板函数(自定义Model) - 用于html中的方法, 需要使用router.SetFuncMap(template.FuncMap{ })
来注册。
这种业务逻辑我们同样也可以放到控制器函数内执行。
如果我们的应用非常简单的话, 我们可以在Controller 里面处理常见的业务逻辑。但是如果我们有一个功能想在多个控制器、或者多个模板里面复用的话, 那么我们就可以把公共的功能单独抽取出来作为一个模块(Model)。Model是逐步抽象的过程, 一般我们会在Model里面封装一些公共的方法让不同Controller以及html模板使用, 也可以在Model中实现和数据库打交道。

对于图中formatAsDate()函数这样的共用业务逻辑, 我们也可以统一将其抽离到models文件夹下, 用文件夹名字命名 package
, 以便统一后端的mvc架构。
M V C 中:
- M(Model)模型层: 即代表我们本小节的model, 主要用于处理数据库层, 以及封装一些V和C共用的 方法 (后端主要用于处理数据库模型给Controller提供crud的API操作)(在后端对于公共方法, 一般会单独再弄一个utils文件来处理或者单独弄一个common来处理, models仅处理数据库相关)。
- V(View)视图层: 代表html模板(前后端分离时,后端可以忽略), 主要用于处理用户界面, 放置渲染用户界面用的逻辑文件。
- C(Controller)控制层: 就是控制器 用于放置路由分组中用到的 handler业务逻辑。(常使用controllers或者是services做其文件夹名)
- 用于处理用户对View的操作, 响应View的事件调用。
- 同时还需要调用Model的接口, 从而修改数据。
以前在web的开发中html一般由后端渲染, 现今由于前后端分离 加上 前端的飞速发展, html的渲染早以转由前端工程师来负责, 并借鉴 .net wpf中 使用的mvvm框架, 发展出了 vue、react等一系列前端框架, 随着这些前端框架的不断壮大, mvc架构也就在前端逐渐没落了。(前端渲染的唯一弊端是SEO困难, 因此如果是项目官网的话, 一般还是交由后端来渲染)
题外话, 前端
目前, 前端开发已经普及了mvvm架构, 通过vm 与 v 直接的双向绑定封装, 使得业务开发更为简洁清晰, 将本来 C 层频繁操作dom等渲染 V 层的逻辑,通过mvvm架构层封装的数据绑定来隐形驱动, 简化了业务逻辑。

