安装
go get -u github.com/gin-gonic/gin
初始化项目并启动服务
go mod init gin-project
package mainimport "github.com/gin-gonic/gin"func main() {r := gin.Default()r.GET("/ping", func(c *gin.Context) {c.JSON(200, gin.H{"message": "pong",})})r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}
上面启动了一个服务器,gin.Default
方法返回一个带有日志和错误捕获中间件的引擎
项目目录
参数
项目目录
main.go中
package main
import ("gin-project/global""gin-project/initialize"
)
func main() {global.GVB_ENG = initialize.InitEngine()if global.GVB_ENG != nil {initialize.InitRouter()global.GVB_ENG.Run()}
}
router/params.go
package routerimport ("gin-project/controller""gin-project/global"
)func InitParamsGroup() {paramsRouter := global.GVB_ENG.Group("/use"){paramsRouter.GET("/uri/:id", controller.ParamsController.GetURI)paramsRouter.GET("/query", controller.ParamsController.GetQuery)paramsRouter.POST("/formdata", controller.ParamsController.GetFomdata)}
}
initialize/router.go
package initializeimport "gin-project/router"func InitRouter() {router.InitParamsGroup()
}
uri参数
文档
uri参数长什么样?->http://fancy_fish.top/123456
在这个url后面的123456就是uri参数
获取uri参数方式一ShouldBindUri
如何获取uri参数呢
- 创建一个结构体字段要和uri占位符一致
- 给结构体对应字段设置关联标签
- 调用
ShouldBindUri
获取即可
paramsRouter := global.GVB_ENG.Group("/use")
paramsRouter.GET("/uri/:id/:name",
GetURI)func (receiver *params) GetURI(c *gin.Context) {type Person struct {ID string `uri:"id" binding:"required"`Name string `uri:"name"`}var p Personif err := c.ShouldBindUri(&p); err != nil {c.JSON(400, gin.H{"msg": err.Error()})return}c.JSON(200, gin.H{"data": p,})
}
获取URI参数方式二Param
func (receiver *params) GetURI(c *gin.Context) {type Person struct {ID string `uri:"id" binding:"required"`Name string `uri:"name"`}var p Personp.ID = c.Param("id")p.Name = c.Param("name")c.JSON(200, gin.H{"data": p,})
}
query参数
query参数长什么样?http://127.0.0.1:8080/use/query?id=12452&name=fancy_fish
paramsRouter.GET("/query", GetQuery)func (receiver *params) GetQuery(c *gin.Context) {type Person struct {ID stringName string}p := Person{}p.ID = c.Query("id")p.Name = c.DefaultQuery("name", "默认Query")c.JSON(200, gin.H{"data": p,})
}
DefaultQuery
当没有获取到指定query参数,会给默认值。如上图所示。
formdata参数
paramsRouter.POST("/formdata",GetFormdata)func (receiver *params) GetFormdata(c *gin.Context) {type Person struct {ID stringName string}p := Person{}p.ID = c.PostForm("id")p.Name = c.DefaultPostForm("name", "默认postform")c.JSON(200, gin.H{"data": p,})
}
DefaultPostForm
当没有获取到指定postform参数,会给默认值。如图所示,代码同上。
上传单个文件FormFile
我们先上传个文件打印一下看看获取到的结果是什么
paramsRouter.POST("/file/upload", controller.ParamsController.GetFile)
func (receiver *params) GetFile(c *gin.Context) {f, err := c.FormFile("file")fmt.Println(f.Size, f.Filename, f.Header)// 266438 1.png map[Content-Disposition:[form-data; name="file"; filename="1.png"] Content-Type:[image/png]]if err != nil {c.String(500, "上传文件失败")}c.JSON(200, gin.H{"data": "",})
}
可以看到可以得到文件的文件名、文件大小、格式等
保存到本地
func (receiver *params) GetFile(c *gin.Context) {f, err := c.FormFile("file")if err != nil {c.String(500, "上传文件失败")return}if err := c.SaveUploadedFile(f, path.Join("./assets", f.Filename)); err != nil {fmt.Println(err.Error(), "文件保存失败")return}c.JSON(200, gin.H{"message": "success","code": 1,"fileName": f.Filename,})
}
上传多个文件MultipartForm
paramsRouter.POST("/files/upload", controller.ParamsController.GetFiles)
func (receiver *params) GetFiles(c *gin.Context) {form, err := c.MultipartForm()if err != nil {c.String(500, "上传文件失败")return}files := form.File["files"]fileNames := make([]string, 0)for index, f := range files {fmt.Println(index, f.Filename, f.Size)fileNames = append(fileNames, f.Filename)if err := c.SaveUploadedFile(f, path.Join("./assets", f.Filename)); err != nil {fmt.Println(err.Error(), "文件保存失败")return}}c.JSON(200, gin.H{"message": "success","code": 1,"fileName": fileNames,})
}
数据绑定
GIN提供我们API可以让我们将客户端传递的参数直接绑定到结构体,我们只需要对结构体字段打标签即可。GIN可以绑定一下几类数据,接下来直接展示。
Gin使用 go-playground/validator/v10 进行验证,可以查看标签用法的全部文档。
我们使用ShouildBindWith
去绑定就行了
ShouldBindJSON绑定JSON
paramsRouter.POST("/bind_json", controller.ParamsController.GetBindJson)
// 控制器
func (receiver *params) GetBindJson(c *gin.Context) {type user struct {Name string `json:"name" binding:"required"`Password uint `json:"password" binding:"required"`}var u userif err := c.ShouldBindJSON(&u); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if u.Name != "fancy_fish" || u.Password != 123 {c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})return}c.JSON(http.StatusOK, gin.H{"message": "成功登录"})
}
ShouldBindUri绑定URI
paramsRouter.GET("/bind_uri/:name/:password", controller.ParamsController.GetBindUri)
func (receiver *params) GetBindUri(c *gin.Context) {type user struct {Name string `uri:"name" binding:"required"`Password uint `uri:"password" binding:"required"`}var u userif err := c.ShouldBindUri(&u); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if u.Name != "fancy_fish" || u.Password != 123 {c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})return}c.JSON(http.StatusOK, gin.H{"message": "成功登录", "data": u})
}
其余的绑定自己查文档体会即可。
参数校验
1.先注册校验器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {// 这里的 key 和 fn 可以不一样最终在 struct 使用的是 keyv.RegisterValidation("ValidatePassword", utils.ValidatePassword)
}
2.定义校验标签
type user struct {Name string `uri:"name" binding:"required"`Password uint `uri:"password" binding:"required,ValidatePassword"`}
3.定义校验函数
package utils
import ("fmt""github.com/go-playground/validator/v10"
)
var ValidatePassword validator.Func = func(fl validator.FieldLevel) bool {fmt.Println("开启验证")return false
}
路由分组
路由分组可以将相同业务类型的路由划分到一起,便于项目维护和开发。
func main() {// 1.创建路由r := gin.Default()// 路由组v1 ,处理GET请求v1 := r.Group("/v1")// {} 是书写规范{v1.GET("/login", login) //相当于/v1/loginv1.GET("/submit", submit) //相当于/v1/submit}v2 := r.Group("/v2"){v2.POST("/login", login) //相当于/v2/loginv2.POST("/submit", submit) //相当于/v2/submit}r.Run(":8000")
}
路由封装
为了提高项目可维护性会将项目结构划分,我们上面的示例都是划分好的。
1.创建router目录管理路由
package routerimport ("gin-project/controller""gin-project/global"
)func InitParamsGroup() {paramsRouter := global.GVB_ENG.Group("/use"){paramsRouter.GET("/uri/:id/:name", controller.ParamsController.GetURI)paramsRouter.GET("/query", controller.ParamsController.GetQuery)paramsRouter.POST("/formdata", controller.ParamsController.GetFormdata)paramsRouter.POST("/file/upload", controller.ParamsController.GetFile)paramsRouter.POST("/files/upload", controller.ParamsController.GetFiles)}
}
2.创建controller抽离控制层
package controllerimport ("fmt""github.com/gin-gonic/gin""path"
)type params struct{}func (receiver *params) GetURI(c *gin.Context) {}
func (receiver *params) GetQuery(c *gin.Context) {}
func (receiver *params) GetFormdata(c *gin.Context) {}
func (receiver *params) GetFile(c *gin.Context) {}
func (receiver *params) GetFiles(c *gin.Context) {}var ParamsController = new(params)
3.创建initialize/router.go初始化路由函数
package initializeimport "gin-project/router"func InitRouter() {router.InitParamsGroup()
}
4.main.go启动服务
r := gin.Default()if r != nil {initialize.InitRouter()r.Run()}
中间件
gin的中间件和js中koa一样都是使用的洋葱圈模型。
tip:中间件的注册一定在路由之前,否则不会生效。
中间件中使用协程
在Gin框架中,当你在中间件或处理程序(handler)中启动新的Goroutine时,需拷贝上下文对象。应该使用只读副本的原因是为了
这样做的目的是什么?1.避免竞态条件(race condition)2.避免上下文污染(context pollution)。
在Gin框架中,每个请求都有一个独立的上下文(Context),用于存储请求相关的信息和数据。而中间件和处理程序是按顺序执行的,它们可能会在同一个请求中启动多个Goroutine。如果你在Goroutine中直接使用原始的上下文,那么这个Goroutine和处理程序之间就会共享同一个上下文对象。这样一来,如果多个Goroutine同时对上下文进行读写操作,就可能引发竞态条件,导致数据不一致或错误的结果。
当多个Goroutine同时对上下文进行修改时,它们可能会相互影响,导致数据被意外覆盖或混乱。
func main() {r := gin.Default()r.GET("/long_async", func(c *gin.Context) {// 创建在 goroutine 中使用的副本cCp := c.Copy()go func() {// 用 time.Sleep() 模拟一个长任务。time.Sleep(5 * time.Second)// 请注意您使用的是复制的上下文 "cCp",这一点很重要log.Println("Done! in path " + cCp.Request.URL.Path)}()})r.GET("/long_sync", func(c *gin.Context) {// 用 time.Sleep() 模拟一个长任务。time.Sleep(5 * time.Second)// 因为没有使用 goroutine,不需要拷贝上下文log.Println("Done! in path " + c.Request.URL.Path)})// 监听并在 0.0.0.0:8080 上启动服务r.Run(":8080")
}
全局中间件
所有请求都会经过此中间件
package main
import ("fmt""time""github.com/gin-gonic/gin"
)
// 定义中间
func GlobalMiddleWare() gin.HandlerFunc {return func(c *gin.Context) {t := time.Now()fmt.Println("中间件开始执行了")// 设置变量到Context的key中,可以通过Get()取c.Set("request", "中间件")status := c.Writer.Status()fmt.Println("中间件执行完毕", status)t2 := time.Since(t)fmt.Println("time:", t2)}
}func main() {// 1.创建路由r := gin.Default()// 注册中间件r.Use(GlobalMiddleWare()){r.GET("/c", func(c *gin.Context) {// 取值req, _ := c.Get("request")fmt.Println("request:", req)// 页面接收c.JSON(200, gin.H{"request": req})})}r.Run()
}
中间件之间传值
比如有两个中间件A和B ,A执行完B执行,B中间件依赖A中间件的某个数据怎么办呢?看代码
// 定义中间
type AMiddleware struct{}func (receiver AMiddleware) CreateAMiddleware() gin.HandlerFunc {return func(c *gin.Context) {fmt.Println("aaaaaa")c.Set("AKey", "AValue")c.Next()}
}
type BMiddleware struct{}
func (receiver BMiddleware) CreateBMiddleware() gin.HandlerFunc {return func(c *gin.Context) {v, exist := c.Get("AKey")if !exist {fmt.Println("没有传递Avalue")} else {fmt.Println(v)}c.Next()}
}func main() {// 1.创建路由r := gin.Default()// 注册中间件r.Use(AMiddleWare(),BMiddleWare()){r.GET("/c", func(c *gin.Context) {// 取值req, _ := c.Get("request")fmt.Println("request:", req)// 页面接收c.JSON(200, gin.H{"request": req})})}r.Run()
}
gin渲染模版
定义模版
- 我们需要在项目根目录下创建template文件夹
- 然后配置gin引擎加载
global.GVB_ENG.LoadHTMLGlob("template/**/*")
- 之后创建
.tmpl
文件
- 添加如下代码
{{ define "header/index.tmpl" }}
<html>
<header><h1>{{ .title }}</h1>
</header>
</html>
{{ end }}
5.控制器,gin.H会将参数传递进去,然后我们可以看到页面。
func (receiver params) GetTemplate(c *gin.Context) {c.HTML(http.StatusOK, "header/index.tmpl", gin.H{"title": "这是传递给模版的参数",})
}