目录
Gin简介
Gin是一个Go语言编写的Web框架,它是一个轻量级的Web框架,提供了诸如路由、中间件、日志、配置管理等功能。其运行速度非常块,适用于API服务、微服务等场景。
Gin的环境搭建
在运行下述语句之前,请先确保有已经执行了 go mod init 命令
1 go get -u github.com /gin-gonic/gin
这里补充一个热加载 的第三方库,其目的是在不重启程序的情况下可以实时更新代码,提高开发效率。
1 go install github.com/gravityblast/fresh@latest
Restful API
Gin框架提供一系列的路由方法,其形式大概为请求路由+ Handler函数
在Handler函数中我们可以返回一些后台数据,比如JSON、HTML、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 package mainimport "github.com/gin-gonic/gin" func main () { router := gin.Default() router.LoadHTMLGlob("templates/*" ) router.GET("/ping" , func (ctx *gin.Context) { ctx.HTML(200 , "ping.html" , gin.H{ "message" : "pong" , }) }) router.GET("/" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "message" : "Hello Gin" , }) }) router.POST("/login" , func (ctx *gin.Context) { ctx.String(200 , "login success" ) }) router.PUT("/user" , func (ctx *gin.Context) { ctx.JSONP(200 , map [string ]interface {}{ "id" : 1 , "name" : "admin" , "age" : 20 , }) }) router.DELETE("/user" , func (ctx *gin.Context) { ctx.XML(200 , gin.H{ "message" : "delete success" , }) }) router.Run() }
放回XML数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 router.DELETE("/user" , func (ctx *gin.Context) { ctx.XML(200 , gin.H{ "message" : "delete success" , }) }) router.GET("/moreXML" , func (ctx *gin.Context) { type Message struct { Name string Age int Message string } var msg Message msg.Name = "admin" msg.Age = 20 msg.Message = "moreXML success" ctx.XML(http.StatusOK, msg) })
返回JSONP数据
1 2 3 4 5 6 7 router.PUT("/user" , func (ctx *gin.Context) { ctx.JSONP(200 , map [string ]interface {}{ "id" : 1 , "name" : "admin" , "age" : 20 , }) })
放回JSON数据
其实与其他都类似,这就不一一举例了,只需要把.XML换成.JSON即可。
HTML渲染
HTML模板都在一个目录下
目录结构如下:
使用步骤如下:
首先创建好模板文件,比如ping.html
1 2 3 4 5 6 7 8 9 10 11 12 13 <!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>templates/ping</title> </head> <body> {{.message}} </body> </html>
然后在主函数中需要提前使用LoadHTMLGlob()或者LoadHTMLFiles()方法加载模板文件
1 router.LoadHTMLGlob("templates/*" )
最后在路由中使用HTML()方法渲染模板
1 2 3 4 5 router.GET("/ping" , func (ctx *gin.Context) { ctx.HTML(200 , "ping.html" , gin.H{ "message" : "pong" , }) })
其中步骤1下的{{.message}}
这个message就是在gin.H中定义的变量,在gin.H中定义的变量可以在模板中使用。
HTML模板分散在各个目录下
目录结构如下:
使用步骤如下:
创建模板的时候,要使用define定义名称
1 2 3 4 5 6 7 8 9 10 11 12 13 {{define "admin/index.html" }} <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>Document</title> </head> <body> {{.title}} </body> </html> {{end}}
注意 :define定义的时候需要加上模板路径
在主函数下需要加载模板文件
1 router.LoadHTMLGlob("templates/**/*" )
注意 :这里的/**/*
表示匹配所有目录下的所有文件,如果在default下还有一级目录,并且想要访问的话就需要"templates// /*"
3. 在路由中使用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 26 27 28 29 30 31 32 33 34 35 36 package mainimport ( "net/http" "github.com/gin-gonic/gin" ) func main () { router := gin.Default() router.LoadHTMLGlob("templates/**/*" ) router.GET("/ping" , func (ctx *gin.Context) { ctx.HTML(200 , "admin/ping.html" , gin.H{ "message" : "pong" , }) }) router.GET("/admin" , func (ctx *gin.Context) { ctx.HTML(200 , "admin/index.html" , gin.H{ "title" : "Admin Page" , }) }) router.GET("/question" , func (ctx *gin.Context) { ctx.HTML(200 , "default/question.html" , gin.H{ "title" : "Question Page" , }) }) router.Run() }
注意 :这里的模板文件名要和路由匹配的路径名一致,比如admin/ping.html
和admin/index.html
而不能是/admin/ping.html/
gin的基本语法
输出变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 {{define "admin/index.html"}} <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > {{.title}} {{.user.Name}} {{.user.Gender}} </body > </html > {{end}}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 user1 := User{ Name: "admin" , Age: 20 , Gender: "male" , } router.GET("/ping" , func (ctx *gin.Context) { ctx.HTML(200 , "admin/ping.html" , gin.H{ "message" : "pong" , }) }) router.GET("/admin" , func (ctx *gin.Context) { ctx.HTML(200 , "admin/index.html" , gin.H{ "title" : "Admin Page" , "user" : user1, }) })
这样在模板中就可以使用user1的Name和Gender变量了
注释
变量
可以在模板中声明变量具体语法如下:
移除空格
有时候我们在使用模板语法的时候会不可避免的引入一下空格或者换行符,这样模板最终渲
染出来的内容可能就和我们想的不一样,这个时候可以使用{{-`语法去除模板内容左侧的所有空白符号, 使用`-}}
去除模板内容右侧的所有空白符号。具体语法如下:
注意 :-要紧挨,同时与模板值之间需要使用空格分隔。
比较函数
布尔函数会将任何类型的零值视为假,其余视为真。
下面是定义为函数的二元比较运算的集合:
eq 如果 arg1 == arg2 则返回真
ne 如果 arg1 != arg2 则返回真
lt 如果 arg1 < arg2 则返回真
le 如果 arg1 <= arg2 则返回真
gt 如果 arg1 > arg2 则返回真
ge 如果 arg1 >= arg2 则返回真
With
使用with来输出结构体
1 2 3 4 5 {{with .user}} <h4 > 姓名:{{.Name}}</h4 > <h4 > 性别:{{.user.Gender}}</h4 > <h4 > 年龄:{{.Age}}</h4 > {{end}}
range
1 2 3 router.GET("/" , func (c *gin.Context) { c.HTML(http.StatusOK, "default/index.html" , map [string ]interface {}{ "hobby" : []string {"吃饭" , "睡觉" , "写代码" }, }) })
1 2 3 {{range $key,$value := .hobby}} <p > {{$value}}</p > {{end}}
条件判断
1 2 3 4 5 6 7 {{if gt .score 90}} 优秀 {{else if gt .score 60}} 及格 {{else}} 不及格 {{end}}
自定义模板函数
1 2 3 4 5 6 7 8 9 10 11 12 router := gin.Default() router.SetFuncMap(template.FuncMap{ "formatDate" : func (t string ) string { t += " hello world" return t }, }) router.LoadHTMLGlob("templates/**/*" )
{{.title | formatDate}}
或者 {{formatDate .title}}
路由详解
获取GET请求传来的值
获取querystring参数
1 2 3 4 5 6 router.GET("/user" , func (ctx *gin.Context) { name := ctx.Query("name" ) age := ctx.DefaultQuery("age" , "0" ) ctx.String(200 , "name: %s, age: %s" , name, age) })
其中defaultQuery就是如果age没有的话就返回0
获取path参数
/user/20
1 2 3 4 r.GET("/user/:uid" , func (ctx *gin.Context) { uid := ctx.Param("uid" ) ctx.String(200 , "userID=%s" , uid) })
获取Post请求的form表单数据
定义一个html的页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {{ define "default/add_user.html" }} <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <form action ="/doAddUser" method ="post" > 用户名:<input type ="text" name ="username" /> 密码: <input type ="password" name ="password" /> <input type ="submit" value ="提交" > </form > </body > </html > {{end}}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { r := gin.Default() r.POST("/user/search" , func (c *gin.Context) { username := c.PostForm("username" ) address := c.PostForm("password" ) c.JSON(http.StatusOK, gin.H{ "message" : "ok" , "username" : username, "password" : address, }) }) r.Run(":8080" ) }
获取JSON参数
当前端请求的数据通过JSON提交时,例如向/json发送一个JSON格式的POST请求,则获取请求参数的方式如下:
1 2 3 4 5 6 7 8 9 10 11 r.POST("/json" , func (c *gin.Context) { b, _ := c.GetRawData() var m map [string ]interface {} _ = json.Unmarshal(b, &m) c.JSON(http.StatusOK, m) })
参数绑定到结构体
为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的 Content-Type识别请求数据类型并利用反射机制 自动提取请求中 QueryString、form 表单、JSON、XML 等参数到结构体中。 下面的示例代码演示了.**ShouldBind()**强大的功能,它能够基于请求自动提取 JSON、form 表单和 QueryString 类型的数据,并把值绑定到指定的结构体对象
shouldBind方法底层实现原理大概就是首先通过读取请求的 Content-Type 头判断请求数据类型,然后根据 Content-Type 头与请求Method确认绑定关系,binding,binding是一个接口类型,有一个方法是bind方法,每种数据类型哦都会实现这接口,这个bind的作用就是读取请求数据,并通过反射的方法遍历结构体字段,然后把请求数据设置到结构体字段中。
ShouldBind会按照下面的顺序解析请求中的数据完成绑定:
如果是 GET 请求,只使用 Form 绑定引擎(query)。
如果是 POST 请求,首先检查 content-type 是否为 JSON 或 XML,然后再使用 Form(form-data)。
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 type User struct { Name string `form:"name" json:"name" xml:"name"` Age int `form:"age" json:"age" xml:"age"` Gender string `form:"gender" json:"gender" xml:"gender"` } var user Userrouter.GET("/answer" , func (ctx *gin.Context) { var user User if err := ctx.ShouldBind(&user); err == nil { ctx.JSON(200 , user) } else { ctx.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) } }) router.POST("/answerJson" , func (ctx *gin.Context) { var user User if err := ctx.ShouldBind(&user); err == nil { fmt.Println(user) ctx.JSON(200 , user) } else { ctx.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) } })
路由分组
为什么需要路由分组,进行路由分组一方面可以方便管理路由,减小一个文件下的代码的冗余性,另一方面可以进行分组开发,就相当于把一个大代码就行分成多个小代码,每个小代码都有自己的功能,这样可以更好的进行开发和维护。
首先目录结构如下:
adminRouters.go如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package routersimport "github.com/gin-gonic/gin" func AdminRoutersInit (r *gin.Engine) { adminRouters := r.Group("/admin" ) { adminRouters.GET("/" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "Welcome to admin page" , }) }) adminRouters.GET("/news" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "news page" , }) }) } }
其他Routers.go类似我就不张贴了
main.go如下:
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 mainimport ( "project/routers" "github.com/gin-gonic/gin" ) type User struct { Name string `form:"name" json:"name" xml:"name"` Age int `form:"age" json:"age" xml:"age"` Gender string `form:"gender" json:"gender" xml:"gender"` } func main () { router := gin.Default() routers.AdminRoutersInit(router) routers.UserRoutersInit(router) routers.ApiRoutersInit(router) router.Run() }
Gin自定义控制器
一个路由的处理函数可以直接放在GET路由中,但是当项目变得很庞大的时候,这样就不妥,所以需要把处理函数放在控制器中,接下来就讲讲控制器
方式一:把函数直接抽离到外面
原来的形式:
1 2 3 4 5 6 adminRouters.GET("/" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "Welcome to admin page" , }) })
使用后的形式:
1 2 3 4 5 6 7 8 func Index (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "Welcome to admin page" , }) } adminRouters.GET("/" , Index)
方式二:把函数抽离到外面,但是以结构体组织,这个结构体就能看出是一个控制器
原来的形式:
1 2 3 4 5 6 adminRouters.GET("/" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "Welcome to admin page" , }) })
使用后的形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package admintype AdminController struct {} func (con *AdminController) Index(ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "Welcome to admin page" , }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "project/admin" "github.com/gin-gonic/gin" ) type User struct { Name string `form:"name" json:"name" xml:"name"` Age int `form:"age" json:"age" xml:"age"` Gender string `form:"gender" json:"gender" xml:"gender"` } func main () { adminRouters.GET("/" , admin.AdminController{}.Index) router.Run() }
此方式可以优化成单例模式
控制器的继承
与结构体继承一致
1 2 3 4 5 6 7 8 9 10 11 12 package adminimport ( "net/http" "github.com/gin-gonic/gin" ) type BaseController struct {} func (c BaseController) Success(ctx *gin.Context) {ctx.String(http.StatusOK, "成功" ) } func (c BaseController) Error(ctx *gin.Context) {ctx.String(http.StatusOK, "失败" ) }
NewsController继承BaseController
1 2 3 4 5 6 7 8 9 package adminimport ( "github.com/gin-gonic/gin" ) type NewsController struct {BaseController } func (c NewsController) Index(ctx *gin.Context) {c.Success(ctx) }
Gin中间件
什么是Gin中间件
Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
通俗的讲 :中间件就是匹配路由前和匹配路由完成后执行的一系列操作
路由中间件
Gin 中的中间件必须是一个 gin.HandlerFunc 类型,配置路由的时候可以传递多个func 回调函数,最后一个 func 回调函数前面触发的方法都可以称为中间件。
ctx.Next()调用该请求的剩余处理程序
中间件里面加上 ctx.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 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 package mainimport ( "fmt" "text/template" "time" "github.com/gin-gonic/gin" ) func UnixToTime (unix int64 ) string { t := time.UnixMicro(unix) return t.Format("2024-11-23 15:04:05" ) } func GetTimeMiddleware (c *gin.Context) { fmt.Println("执行时间中间件" ) startTime := time.Now().UnixNano() c.Next() endTime := time.Now().UnixNano() fmt.Println("路由执行时间: " , endTime-startTime, "ns" ) } func main () { r := gin.Default() r.SetFuncMap(template.FuncMap{ "UnixToTime" : UnixToTime, }) r.LoadHTMLGlob("templates/**/*" ) r.GET("/getdog" , GetTimeMiddleware, func (c *gin.Context) { fmt.Println("开始查询数据库" ) time.Sleep(time.Second) c.JSON(200 , gin.H{ "message" : "小狗有一百条" , }) }) r.Run() }
程序输出:
1 2 3 执行时间中间件 开始查询数据库 路由执行时间: 1000162500 ns
明显可以看到其执行顺序是先执行中间件,执行到c.Next()后执行剩余的函数(可以是路由处理函数也可以是其他的中间件函数 ),如何在处理剩余中间件的未执行部分(c.Next()后面)
**c.Abort()**这个函数表示中止路由处理函数,但是还会继续执行c.Abort()后面的函数
一个路由配置多个中间件的执行顺序
看如下例子:
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 package mainimport ( "fmt" "text/template" "time" "github.com/gin-gonic/gin" ) func UnixToTime (unix int64 ) string { t := time.UnixMicro(unix) return t.Format("2024-11-23 15:04:05" ) } func ExampleMiddleware (c *gin.Context) { fmt.Println("执行例子中间件" ) c.Next() fmt.Println("例子中间件执行完毕" ) } func GetTimeMiddleware (c *gin.Context) { fmt.Println("执行时间中间件" ) startTime := time.Now().UnixNano() c.Next() endTime := time.Now().UnixNano() fmt.Println("路由执行时间: " , endTime-startTime, "ns" ) } func main () { r := gin.Default() r.SetFuncMap(template.FuncMap{ "UnixToTime" : UnixToTime, }) r.LoadHTMLGlob("templates/**/*" ) r.GET("/getdog" , ExampleMiddleware, GetTimeMiddleware, func (c *gin.Context) { fmt.Println("开始查询数据库" ) time.Sleep(time.Second) fmt.Println("查询数据库完毕" ) c.JSON(200 , gin.H{ "message" : "小狗有一百条" , }) }) r.Run() }
输出结果:
1 2 3 4 5 6 16 : 28 : 45 app | 执行例子中间件 16 : 28 : 45 app | 执行时间中间件 开始查询数据库 16 : 28 : 46 app | 查询数据库完毕 16 : 28 : 46 app | 路由执行时间: 1000586500 ns 例子中间件执行完毕
可以发现这执行顺序是先处理最前面的中间件,依次往后处理,然后执行.Next()是从后往前,类似于栈的执行顺序
全局中间件
使用c.Use()方法可以注册全局中间件,全局中间件会在每个请求处理函数之前执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 c.Use(ExampleMiddleware, GetTimeMiddleware) r.GET("/getdog" , func (c *gin.Context) { fmt.Println("开始查询数据库" ) time.Sleep(time.Second) fmt.Println("查询数据库完毕" ) c.JSON(200 , gin.H{ "message" : "小狗有一百条" , }) })
在路由分组中配置中间件
一共有两种方式:
方式1:直接在Group中加入中间件
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 package routersimport ( "project/middleware" "github.com/gin-gonic/gin" ) func UserRoutersInit (r *gin.Engine) { adminRouters := r.Group("/user" , middleware.InitMiddleware) { adminRouters.GET("/" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "Welcome to admin page" , }) }) adminRouters.GET("/play" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "play page" , }) }) } }
方式2:使用Use()
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 routersimport ( "project/middleware" "github.com/gin-gonic/gin" ) func UserRoutersInit (r *gin.Engine) { adminRouters := r.Group("/user" ) adminRouters.Use(middleware.InitMiddleware) { adminRouters.GET("/" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "Welcome to admin page" , }) }) adminRouters.GET("/play" , func (ctx *gin.Context) { ctx.JSON(200 , gin.H{ "path" : ctx.Request.URL.Path, "message" : "play page" , }) }) } }
中间件和对应控制器之间共享数据
设置值
获取值
中间件设置值1 2 3 4 5 6 7 func InitAdminMiddleware (ctx *gin.Context) {fmt.Println("路由分组中间件" ) ctx.Set("username" , "张三" ) ctx.Next() }
控制器获取值1 2 3 4 5 func (c UserController) Index(ctx *gin.Context) {username, _ := ctx.Get("username" ) fmt.Println(username) ctx.String(http.StatusOK, "这是用户首页 111" ) }
这样就可以在控制器中通过Get获取值
中间件注意事项
gin.Default()默认使用了 Logger 和 Recovery 中间件,其中:
• Logger 中间件将日志写入 gin.DefaultWriter,即使配置了 GIN_MODE=release。
• Recovery 中间件会 recover 任何 panic。如果有 panic 的话,会写入 500 响应码。
如果不想使用上面两个默认的中间件,可以使用 gin.New()新建一个没有任何默认中间件的
路由。
gin 中间件中使用goroutine
当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(c *gin.Context) , 必须使用其只读副本(c.Copy())
例如:
1 2 3 4 5 6 7 8 9 10 r.GET("/" , func (c *gin.Context) { cCp := c.Copy() go func () { time.Sleep(5 * time.Second) fmt.Println("Done! in path " + cCp.Request.URL.Path) }() c.String(200 , "首页" ) })
Why? Gin的上下文是非线程安全的
Gin自定义Model
什么是Model
如果我们的应用非常简单的话,我们可以在 Controller 里面处理常见的业务逻辑。但是如果我们有一个功能想在多个控制器、或者多个模板里面复用 的话,那么我们就可以把公共的功能单独抽取出来作为一个模块(Model) 。 Model 是逐步抽象的过程,一般我们会在 Model里面封装一些公共的方法让不同 Controller 使用,也可以在 Model 中实现和数据库打交道
Model里面封装公共的方法
例如TimeToUnix 和 UnixToTime 这两个方法在控制器和模板中都需要使用到,所以可以把它提取出来成为一个model,例如:
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 package modelsimport "time" func UnixToTime (unix int64 ) string { t := time.Unix(unix, 0 ) return t.Format("2006-01-02 15:04:05" ) } func TimeToUnix (t string ) int64 { template := "2006-01-02 15:04:05" tm, err := time.ParseInLocation(template, t, time.Local) if err != nil { return 0 } return tm.Unix() } func GetCurrentTime () int64 { return time.Now().Unix() }
注意 : 当使用时间模板的时候,其值必须是2006年1月2日 15:04:05这个时间点,否则解析出来的会出错,但是格式可以不一样,可以是2006-01-02 15:04:05
或者 2006/01/02 15:04:05
在Model中注册全局模板函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func UnixToTime (unix int64 ) string { t := time.Unix(unix, 0 ) return t.Format("2006-01-02 15:04:05" ) } r := gin.Default() r.SetFuncMap(template.FuncMap{ "unixToDate" : models.UnixToDate, }) <h2>{{.now | unixToDate}}</h2>
这样就可以在模板中使用unixToDate
函数了
Gin文件上传
功能:将前端上传的文件保存到服务器中
单个文件上传
前端使用HTML表单上传文件(需要在上传文件的 form 表单上面需要加入 enctype=“multipart/form-data”)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {{ define "admin/user/add.html"}} <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <form action = "/admin/user/doUpload" method ="post" enctype ="multipart/form-data" > 用户名: <input type = "text" name = "username" placeholder ="请输入用户名" > <br > 头 像: <input type = "file" name = "face" > <br > <input type = "submit" value = "提交" > </form > </body > </html > {{ end }}
编写业务逻辑
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 adminRouters.GET("/add" , admin.UserController{}.Add) type UserController struct {} func (con UserController) Add(c *gin.Context) { c.HTML(http.StatusOK, "admin/user/add.html" , gin.H{}) } adminRouters.POST("/user/doUpload" , admin.DoAdd) func DoAdd (c *gin.Context) { username := c.PostForm("username" ) file, err := c.FormFile("face" ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message" : err.Error(), }) return } dst := path.Join("./static/uploads" , file.Filename) c.SaveUploadedFile(file, dst) c.JSON(200 , gin.H{ "success" : true , "path" : dst, "username" : username, }) }
多个文件上传
同名文件的上传:
前端使用HTML表单上传文件(需要在上传文件的 form 表单上面需要加入 enctype=“multipart/form-data”)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 {{ define "admin/user/add.html"}} <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <form action = "/admin/user/doUpload" method ="post" enctype ="multipart/form-data" > 用户名: <input type = "text" name = "username" placeholder ="请输入用户名" > <br > 头 像1: <input type = "file" name = "face[]" > <br > 头 像2: <input type = "file" name = "face[]" > <br > 头 像3: <input type = "file" name = "face[]" > <br > <input type = "submit" value = "提交" > </form > </body > </html > {{ end }}
编写业务逻辑
与单文件相比,修改的主要是:
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 package adminimport ( "net/http" "path" "github.com/gin-gonic/gin" ) func DoAdd (c *gin.Context) { username := c.PostForm("username" ) form, err := c.MultipartForm() files := form.File["face[]" ] if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "message" : err.Error(), }) return } for _, file := range files { dst := path.Join("./static/uploads" , file.Filename) c.SaveUploadedFile(file, dst) } c.JSON(200 , gin.H{ "success" : true , "username" : username, }) }
注意 : c.MultipartForm()会返回一个 MultipartForm 对象,该对象包含了上传的文件和其他表单数据
其内容如下:
1 &multipart.Form {Value :map [string][]string{"username" :[]string {"2" }}, File :map [string][]*multipart.FileHeader {"face[]" :[]*multipart .FileHeader {(*multipart.FileHeader )(0xc000290180 ), (*multipart.FileHeader )(0xc0002901e0 ), (*multipart.FileHeader )(0xc000290240 )}}}
按日期存储文件
假设现在要实现这样的功能,将前端上传的图片获取到本地,并且存储在当天日期的文件下,文件名使用当时的时间戳
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 package adminimport ( "log" "os" "path" "project/models" "strconv" "github.com/gin-gonic/gin" ) func DoAdd (c *gin.Context) { file, err := c.FormFile("face" ) if err != nil { log.Println(err) c.String(400 , "上传文件失败" ) return } allowedExts := map [string ]bool { ".jpg" : true , ".png" : true , ".jpeg" : true , ".gif" : true , } extName := path.Ext(file.Filename) if ok := allowedExts[extName]; !ok { c.String(400 , "上传文件格式不正确" ) return } curDay := models.GetDay() dir := "./static/upload/" + curDay os.MkdirAll(dir, 0666 ) fileName := strconv.FormatInt(models.GetCurrentTime(), 10 ) + extName dst := path.Join(dir, fileName) c.SaveUploadedFile(file, dst) c.JSON(200 , gin.H{ "message" : "上传成功" , }) }
总结
获取上传文件的主要步骤是:
使用c.FormFile(前端定义的name属性)获取上传的文件
使用c.SaveUploadedFile(获取的文件,目的地)保存文件到本地
这期间可以做任何事,比如自定义存储路径和文件名等等,其中c指的是gin.Context对象,可以获取请求信息,设置响应信息等等。
Gin的Cookie相关
什么是Cookie
HTTP 是无状态协议。简单地说,当你浏览了一个页面,然后转到同一个网站的另一个页面,服务器无法认识到这是同一个浏览器在访问同一个网站。每一次的访问,都是没有任何关系的。如果我们要实现多个页面之间共享数据的话我们就可以使用 Cookie 或者 Session 实现
cookie 是存储于访问者计算机的浏览器中。可以让我们用同一个浏览器访问同一个域名的时候共享数据。
Cookie可以实现的功能
实现用户登录状态保持
保存用户浏览历史记录
猜你喜欢,智能推荐
电商网站的购物车功能
设置Cookie
1 c.SetCookie(name , value string, maxAge int , path , domain string, secure, httpOnly bool )
第一个参数 key
第二个参数 value
第三个参数 过期时间.如果只想设置 Cookie 的保存路径而不想设置存活时间,可以在第三个参数中传递 nil
第四个参数 cookie 的路径
第五个参数 cookie 的路径 Domain 作用域 本地调试配置成 localhost , 正式上线配置成域名
第六个参数是 secure ,当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效
第七个参数 httpOnly,是微软对 COOKIE 做的扩展。如果在 COOKIE 中设置了“httpOnly”属性,则通过程序(JS 脚本、applet 等)将无法读取到 COOKIE 信息,防止 XSS 攻击产生
获取Cookie
1 c.Cookie(name string ) (value string , err error )
删除Cookie
删除Cookie 可以重新设置Cookie,让过期时间为-1即可
多个二级域名共享cookie
分别把 a.pigcanstudy.com 和 b.pigcanstudy.com 解析到我们的服务器
我们想的是用户在 a.pigcanstudy.com 中设置 Cookie 信息后在 b.pigcanstudy.com 中获取刚才设置的cookie,也就是实现多个二级域名共享 cookie
这时候可以这样使用
1 c .SetCookie("usrename" , "张三" , 3600 , "/" , ".itying.com" , false , true )
案例使用
需求如下:
在 /setcookie 路由上,当你访问这个路由时,会设置一个名为 username 的cookie,值为 小鸟,并且设置过期时间为3600秒(1小时)。
然后在 /getcookie 路由上,当你访问这个路由时,会获取刚才设置的cookie,并返回给你。
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 package cookieimport "github.com/gin-gonic/gin" type CookieController struct {} func (con CookieController) SetCookie(c *gin.Context) { c.SetCookie("username" , "小鸟" , 3600 , "/" , "localhost" , false , false ) c.String(200 , "设置cookie成功" ) } func (con CookieController) GetCookie(c *gin.Context) { str, _ := c.Cookie("username" ) c.JSON(200 , gin.H{ "username" : str, "success" : true , }) } package routersimport ( "project/controllers/cookie" "github.com/gin-gonic/gin" ) func CookieRouterInit (r *gin.Engine) { r.GET("/setcookie" , cookie.CookieController{}.SetCookie) r.GET("/getcookie" , cookie.CookieController{}.GetCookie) } routers.CookieRouterInit(r)
Gin的Session相关
什么是Session
session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 session保存在服务器上。
Session的工作流程
当客户端浏览器第一次访问服务器并发送请求时,服务器端会创建一个 session 对象,生成一个类似于 key,value 的键值对,然后将 value 保存到服务器 将 key(cookie)返回到浏览器(客户)端。浏览器下次访问时会携带 key(cookie),找到对应的 session(value)。
Gin中使用Session
Gin 官方没有给我们提供 Session 相关的文档,这个时候我们可以使用第三方的 Session 中间件来实现
https://github.com/gin-contrib/sessions
gin-contrib/sessions 中间件支持的存储引擎:
cookie
memestore
redis
memcached
mongodb
1 go get -u github.com /gin-contrib/sessions
基于Cookie存储Session
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 store := cookie.NewStore([]byte ("secret11111" )) r.Use(sessions.Sessions("mysession" , store)) r.GET("/" , func (c *gin.Context) { session := sessions.Default(c) session.Options(sessions.Options{ MaxAge: 3600 * 6 , }) session.Set("username" , "张三" ) session.Save() c.String(200 , "session设置成功" ) }) r.GET("/getsession" , func (c *gin.Context) { session := sessions.Default(c) username := session.Get("username" ) c.String(200 , "username: %s" , username) })
基于Redis存储Session
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 store1, _ := redis.NewStore(10 , "tcp" , "localhost:6379" , "" , []byte ("secret" )) r.Use(sessions.Sessions("mysession" , store1)) r.GET("/" , func (c *gin.Context) { session := sessions.Default(c) session.Set("username" , "李四" ) session.Save() c.JSON(200 , gin.H{"username" : session.Get("username" )}) }) r.GET("/user" , func (c *gin.Context) { session := sessions.Default(c) username := session.Get("username" ) c.JSON(200 , gin.H{"username" : username}) })
总结
session中间件的使用方法是:
创建一个存储引擎,比如基于cookie的存储引擎,或者基于redis的存储引擎(使用xxx.NewStore())
设置session中间件,参数是session的名字(也是浏览器的cookie),以及存储引擎(使用r.Use(sessions.Sessions(“mysession”, store)))
使用session中间件,通过session.Get()或者session.Set()来设置并使用session.Save()来保存或者获取session的值(具体看业务需求)
gin中使用go-ini来加载.ini配置文件
go-ini介绍
go-ini使用