目录

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

  1. Gin框架提供一系列的路由方法,其形式大概为请求路由+ Handler函数
  2. 在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 main

import "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",
})
})

// 启动Web服务
router.Run()
}

放回XML数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 方式1
router.DELETE("/user", func(ctx *gin.Context) {
ctx.XML(200, gin.H{
"message": "delete success",
})
})

router.GET("/moreXML", func(ctx *gin.Context) {
// 方式2使用结构体
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模板都在一个目录下

目录结构如下:
alt text

使用步骤如下:

  1. 首先创建好模板文件,比如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>
  1. 然后在主函数中需要提前使用LoadHTMLGlob()或者LoadHTMLFiles()方法加载模板文件
1
router.LoadHTMLGlob("templates/*")
  1. 最后在路由中使用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模板分散在各个目录下

  • 目录结构如下:
    alt text

  • 使用步骤如下:

  1. 创建模板的时候,要使用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. 在主函数下需要加载模板文件
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 main

import (
"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",
})
})

// 启动Web服务
router.Run()
}

  • 注意:这里的模板文件名要和路由匹配的路径名一致,比如admin/ping.htmladmin/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}}
  • 其中user是一个结构体变量
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变量了

注释

1
{{/* 注释内容 */}}

变量

可以在模板中声明变量具体语法如下:

1
{{$obj := .title}}
  • 声明变量,并将.title的值赋值给它
1
{{$obj}}
  • 输出变量的值

移除空格

有时候我们在使用模板语法的时候会不可避免的引入一下空格或者换行符,这样模板最终渲
染出来的内容可能就和我们想的不一样,这个时候可以使用{{-`语法去除模板内容左侧的所有空白符号, 使用`-}}去除模板内容右侧的所有空白符号。具体语法如下:

1
{{- .title -}}

注意:-要紧挨,同时与模板值之间需要使用空格分隔。

比较函数

布尔函数会将任何类型的零值视为假,其余视为真。
下面是定义为函数的二元比较运算的集合:
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}}
  • 通过PostForm来获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
//Default返回一个默认的路由引擎
r := gin.Default()
r.POST("/user/search", func(c *gin.Context) {
// DefaultPostForm取不到值时会返回指定的默认值
//username := c.DefaultPostForm("username", "小王子")
username := c.PostForm("username")
address := c.PostForm("password")
//输出json结果给调用方
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() // 从c.Request.Body读取请求数据
// 定义map或结构体
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 User

router.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()})
}
})

路由分组

  • 为什么需要路由分组,进行路由分组一方面可以方便管理路由,减小一个文件下的代码的冗余性,另一方面可以进行分组开发,就相当于把一个大代码就行分成多个小代码,每个小代码都有自己的功能,这样可以更好的进行开发和维护。

首先目录结构如下:
alt text

  • 路由分组的使用方法如下:

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 routers

import "github.com/gin-gonic/gin"

func AdminRoutersInit(r *gin.Engine) {
// Admin routers
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 main

import (
"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)

// 启动Web服务
router.Run()
}

Gin自定义控制器

一个路由的处理函数可以直接放在GET路由中,但是当项目变得很庞大的时候,这样就不妥,所以需要把处理函数放在控制器中,接下来就讲讲控制器

  1. 方式一:把函数直接抽离到外面

原来的形式:

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. 方式二:把函数抽离到外面,但是以结构体组织,这个结构体就能看出是一个控制器

原来的形式:

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 admin

// AdminController.go 文件中
type 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 main

import (
"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)

// 启动Web服务
router.Run()
}

此方式可以优化成单例模式

控制器的继承

与结构体继承一致

1
2
3
4
5
6
7
8
9
10
11
12
package admin
import ( "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 admin
import ( "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 main

import (
"fmt"
"text/template"
"time"

"github.com/gin-gonic/gin"
)

// 把Unix时间戳转换为时间字符串
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/**/*")

// // 配置静态Web目录 第一个参数表示路由,第二个参数表示映射的目录
// r.Static("/static", "./static")

// 注册路由
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 main

import (
"fmt"
"text/template"
"time"

"github.com/gin-gonic/gin"
)

// 把Unix时间戳转换为时间字符串
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/**/*")

// // 配置静态Web目录 第一个参数表示路由,第二个参数表示映射的目录
// r.Static("/static", "./static")

// 注册路由
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. 方式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 routers

import (
"project/middleware"

"github.com/gin-gonic/gin"
)

func UserRoutersInit(r *gin.Engine) {
// Admin routers
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",
})
})
}
}
  1. 方式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 routers

import (
"project/middleware"

"github.com/gin-gonic/gin"
)

func UserRoutersInit(r *gin.Engine) {
// Admin routers
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
    c.Set("name", "jack")
  • 获取值
    1
    name := c.Get("name")
  • 中间件设置值
    1
    2
    3
    4
    5
    6
    7
    func InitAdminMiddleware(ctx *gin.Context) {
    fmt.Println("路由分组中间件")
    // 可以通过 ctx.Set 在请求上下文中设置值,后续的处理函数能够取到该值
    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 默认中间件

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() {
// simulate a long task with time.Sleep(). 5 seconds
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 models

import "time"

// 把Unix时间戳转换为时间字符串
func UnixToTime(unix int64) string {
// 根据Unix时间戳生成时间对象
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"
// 解析时间字符串,time.Local表示本地时间的时区
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
// models/tools.go

// 把Unix时间戳转换为时间字符串
func UnixToTime(unix int64) string {
// 根据Unix时间戳生成时间对象
t := time.Unix(unix, 0)

// 格式化时间字符串并返回
return t.Format("2006-01-02 15:04:05")

}

// main.go
r := gin.Default()
r.SetFuncMap(template.FuncMap{ "unixToDate": models.UnixToDate, })

// 模板
<h2>{{.now | unixToDate}}</h2>

这样就可以在模板中使用unixToDate函数了

Gin文件上传

功能:将前端上传的文件保存到服务器中

单个文件上传

  1. 前端使用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. 编写业务逻辑
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
// userRouter.go 
adminRouters.GET("/add", admin.UserController{}.Add)

// userController.go
type UserController struct {
}

func (con UserController) Add(c *gin.Context) {
c.HTML(http.StatusOK, "admin/user/add.html", gin.H{})
}

// adminRouter.go
adminRouters.POST("/user/doUpload", admin.DoAdd)

// adminController.go
func DoAdd(c *gin.Context) {
// 从表单中获取用户名
username := c.PostForm("username")

// 获取上传的文件名,对应html中的name属性
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)

// 保存文件到本地dst路径下
c.SaveUploadedFile(file, dst)

c.JSON(200, gin.H{
"success": true,
"path": dst,
"username": username,
})
}

多个文件上传

同名文件的上传:

  1. 前端使用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. 编写业务逻辑

与单文件相比,修改的主要是:

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 admin

import (
"net/http"
"path"

"github.com/gin-gonic/gin"
)

// 执行获取前端上传的文件和下载到本地
func DoAdd(c *gin.Context) {
// 从表单中获取用户名
username := c.PostForm("username")

// 会返回一个 MultipartForm 对象,该对象包含了上传的文件和其他表单数据
form, err := c.MultipartForm()

// 获取上传的文件名,对应html中的name属性
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)
// 保存文件到本地dst路径下
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 admin

import (
"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
}

// 检查文件的后缀名是否是jpg,.png,.jpeg,.gif

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": "上传成功",
})

}

总结

获取上传文件的主要步骤是:

  1. 使用c.FormFile(前端定义的name属性)获取上传的文件
  2. 使用c.SaveUploadedFile(获取的文件,目的地)保存文件到本地

这期间可以做任何事,比如自定义存储路径和文件名等等,其中c指的是gin.Context对象,可以获取请求信息,设置响应信息等等。

Gin的Cookie相关

什么是Cookie

  • HTTP 是无状态协议。简单地说,当你浏览了一个页面,然后转到同一个网站的另一个页面,服务器无法认识到这是同一个浏览器在访问同一个网站。每一次的访问,都是没有任何关系的。如果我们要实现多个页面之间共享数据的话我们就可以使用 Cookie 或者 Session 实现
  • cookie 是存储于访问者计算机的浏览器中。可以让我们用同一个浏览器访问同一个域名的时候共享数据。

Cookie可以实现的功能

  1. 实现用户登录状态保持
  2. 保存用户浏览历史记录
  3. 猜你喜欢,智能推荐
  4. 电商网站的购物车功能

设置Cookie

1
c.SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
  1. 第一个参数 key
  2. 第二个参数 value
  3. 第三个参数 过期时间.如果只想设置 Cookie 的保存路径而不想设置存活时间,可以在第三个参数中传递 nil
  4. 第四个参数 cookie 的路径
  5. 第五个参数 cookie 的路径 Domain 作用域 本地调试配置成 localhost , 正式上线配置成域名
  6. 第六个参数是 secure ,当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效
  7. 第七个参数 httpOnly,是微软对 COOKIE 做的扩展。如果在 COOKIE 中设置了“httpOnly”属性,则通过程序(JS 脚本、applet 等)将无法读取到 COOKIE 信息,防止 XSS 攻击产生

获取Cookie

1
c.Cookie(name string) (value string, err error)

删除Cookie

删除Cookie 可以重新设置Cookie,让过期时间为-1即可

多个二级域名共享cookie

  1. 分别把 a.pigcanstudy.comb.pigcanstudy.com 解析到我们的服务器
  2. 我们想的是用户在 a.pigcanstudy.com 中设置 Cookie 信息后在 b.pigcanstudy.com 中获取刚才设置的cookie,也就是实现多个二级域名共享 cookie

这时候可以这样使用

1
c.SetCookie("usrename", "张三", 3600, "/", ".itying.com", false, true)

案例使用

  • 需求如下:
    1. 在 /setcookie 路由上,当你访问这个路由时,会设置一个名为 username 的cookie,值为 小鸟,并且设置过期时间为3600秒(1小时)。
    2. 然后在 /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
// cookieRouter.go
package cookie

import "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,
})
}

// ------------------------------------------------------------------------------

// cookieRouters.go
package routers

import (
"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)
}

// main.go

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
// 创建基于 cookie 的存储引擎,secret11111 参数是用于加密的密钥
store := cookie.NewStore([]byte("secret11111"))

// 设置 session 中间件,参数 mysession,指的是 session 的名字,也是 cookie 的名字
// 配置中间件 store 是前面创建的存储引擎,我们可以替换成其他存储引擎
r.Use(sessions.Sessions("mysession", store))

r.GET("/", func(c *gin.Context) {
// 初始化 session对象
session := sessions.Default(c)

// 设置过期时间
session.Options(sessions.Options{
MaxAge: 3600 * 6,
})

// 设置一个keyvalue
session.Set("username", "张三")

session.Save()

c.String(200, "session设置成功")
})

r.GET("/getsession", func(c *gin.Context) {
// 初始化session对象
session := sessions.Default(c)

// 获取value
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
// 初始化基于 redis 的存储引擎
// 参数说明:
// 第 1 个参数 - redis 最大的空闲连接数
// 第 2 个参数 - 数通信协议 tcp 或者 udp
// 第 3 个参数 - redis 地址, 格式,host:port
// 第 4 个参数 - redis 密码
// 第 5 个参数 - session 加密密钥
store1, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))

// 初始化 session 中间件,musession是会话名称
r.Use(sessions.Sessions("mysession", store1))
r.GET("/", func(c *gin.Context) {
session := sessions.Default(c)
// 这是标识一个用户的用户名(cookie)
session.Set("username", "李四")
session.Save()
c.JSON(200, gin.H{"username": session.Get("username")})
})
r.GET("/user", func(c *gin.Context) {
// 初始化 session 对象
session := sessions.Default(c)
// 通过 session.Get 读取 session 值
username := session.Get("username")
c.JSON(200, gin.H{"username": username})
})

总结

session中间件的使用方法是:

  1. 创建一个存储引擎,比如基于cookie的存储引擎,或者基于redis的存储引擎(使用xxx.NewStore())
  2. 设置session中间件,参数是session的名字(也是浏览器的cookie),以及存储引擎(使用r.Use(sessions.Sessions(“mysession”, store)))
  3. 使用session中间件,通过session.Get()或者session.Set()来设置并使用session.Save()来保存或者获取session的值(具体看业务需求)

gin中使用go-ini来加载.ini配置文件

go-ini介绍

go-ini使用