安装
sh
go get -u github.com/gin-gonic/gin
初始化项目并启动服务
go mod init gin-project
go
package main
import "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中
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
go
package router
import (
"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
go
package initialize
import "gin-project/router"
func InitRouter() {
router.InitParamsGroup()
}
uri参数
uri参数长什么样?->http://fancy_fish.top/123456
在这个url后面的123456就是uri参数
获取uri参数方式一ShouldBindUri
如何获取uri参数呢
- 创建一个结构体字段要和uri占位符一致
- 给结构体对应字段设置关联标签
- 调用
ShouldBindUri
获取即可
go
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 Person
if err := c.ShouldBindUri(&p); err != nil {
c.JSON(400, gin.H{"msg": err.Error()})
return
}
c.JSON(200, gin.H{
"data": p,
})
}
获取URI参数方式二Param
go
func (receiver *params) GetURI(c *gin.Context) {
type Person struct {
ID string `uri:"id" binding:"required"`
Name string `uri:"name"`
}
var p Person
p.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
go
paramsRouter.GET("/query", GetQuery)
func (receiver *params) GetQuery(c *gin.Context) {
type Person struct {
ID string
Name string
}
p := Person{}
p.ID = c.Query("id")
p.Name = c.DefaultQuery("name", "默认Query")
c.JSON(200, gin.H{
"data": p,
})
}
DefaultQuery
当没有获取到指定query参数,会给默认值。如上图所示。
formdata参数
go
paramsRouter.POST("/formdata",GetFormdata)
func (receiver *params) GetFormdata(c *gin.Context) {
type Person struct {
ID string
Name string
}
p := Person{}
p.ID = c.PostForm("id")
p.Name = c.DefaultPostForm("name", "默认postform")
c.JSON(200, gin.H{
"data": p,
})
}
DefaultPostForm
当没有获取到指定postform参数,会给默认值。如图所示,代码同上。
上传单个文件FormFile
我们先上传个文件打印一下看看获取到的结果是什么
go
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": "",
})
}
可以看到可以得到文件的文件名、文件大小、格式等
保存到本地
go
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
go
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
go
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 user
if 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
go
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 user
if 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.先注册校验器
go
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 这里的 key 和 fn 可以不一样最终在 struct 使用的是 key
v.RegisterValidation("ValidatePassword", utils.ValidatePassword)
}
2.定义校验标签
go
type user struct {
Name string `uri:"name" binding:"required"`
Password uint `uri:"password" binding:"required,ValidatePassword"`
}
3.定义校验函数
go
package utils
import (
"fmt"
"github.com/go-playground/validator/v10"
)
var ValidatePassword validator.Func = func(fl validator.FieldLevel) bool {
fmt.Println("开启验证")
return false
}
路由分组
路由分组可以将相同业务类型的路由划分到一起,便于项目维护和开发。
go
func main() {
// 1.创建路由
r := gin.Default()
// 路由组v1 ,处理GET请求
v1 := r.Group("/v1")
// {} 是书写规范
{
v1.GET("/login", login) //相当于/v1/login
v1.GET("/submit", submit) //相当于/v1/submit
}
v2 := r.Group("/v2")
{
v2.POST("/login", login) //相当于/v2/login
v2.POST("/submit", submit) //相当于/v2/submit
}
r.Run(":8000")
}
路由封装
为了提高项目可维护性会将项目结构划分,我们上面的示例都是划分好的。
1.创建router目录管理路由
go
package router
import (
"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抽离控制层
go
package controller
import (
"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初始化路由函数
go
package initialize
import "gin-project/router"
func InitRouter() {
router.InitParamsGroup()
}
4.main.go启动服务
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同时对上下文进行修改时,它们可能会相互影响,导致数据被意外覆盖或混乱。
go
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")
}
全局中间件
所有请求都会经过此中间件
go
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中间件的某个数据怎么办呢?看代码
go
// 定义中间
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引擎加载
go
global.GVB_ENG.LoadHTMLGlob("template/**/*")
- 之后创建
.tmpl
文件
- 添加如下代码
go
{{ define "header/index.tmpl" }}
<html>
<header>
<h1>
{{ .title }}
</h1>
</header>
</html>
{{ end }}
5.控制器,gin.H会将参数传递进去,然后我们可以看到页面。
go
func (receiver params) GetTemplate(c *gin.Context) {
c.HTML(http.StatusOK, "header/index.tmpl", gin.H{
"title": "这是传递给模版的参数",
})
}