Gin 框架复习 | 青训营笔记

187 阅读20分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第 6 篇笔记

因为大项目里要用到 Gin 框架,而这东西我已经有点忘了,故特来复习


P1:环境搭建、简单的路由配置

一、Gin 是什么

Gin 是一个用 Go (Golang) 编写的 web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架, 由于 httprouter,速度提高了近 40 倍。

同时,它目前在 GitHub 上已经有了 50k+ 的 Star,可谓是非常热门

二、环境搭建

注意:以下步骤需要全程魔法上网

  1. 在 VScode 中安装 Go 扩展

  2. 新建 test 文件夹,并在其中新建一个 main.go

这时会提醒你下载很多工具,确认下载即可,这里就看你的网速了,记得魔法上网

  1. 终端里切换到 test 目录,并执行下面的命令
go mod init test
go get -u github.com/gin-gonic/gin
  1. 新建 main.go ,并输入
import "github.com/gin-gonic/gin"
  1. 再在终端中执行命令
go mod tidy

这一步执行完毕后,目录中应该会有 go.mod 和 go.sum 两个文件

  1. 用下面的代码测试一下
package main

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

func main() {
	// 创建一个默认的路由引擎
	r := gin.Default()
	// 配置路由
	r.GET("/", func(c *gin.Context) {
		c.String(200, "%v", "Hello World")
	})
	// 启动 web 服务
	r.Run()
}

使用go run main.go运行,访问本机的 8080 端口能看见 Hello World

至此,环境搭建完毕

来分析一下上面的代码:

	r.GET("/", func(c *gin.Context) {
		c.String(200, "%v", "Hello World")
	})

r.GET 表示用于处理 GET 请求, "/" 指的是要处理的路径,一个斜杠就是根目录,后面的跟着一个函数(此例是匿名函数)用于处理若访问了这个路径的操作,这里就返回 200(HTTP状态码) 和 一个字符串

可以复制粘贴出多个 r.GET 来处理其他路径,当然,也可以换成 POST PUT DELETE 来处理对应的请求(建议安装 postman 来更为方便地发送请求

r.Run()

这句用于启动服务,默认的端口是8080,可以通过传入端口号来修改而在其他端口启动服务,例如:

r.Run(":8000")

P2:响应数据 c.String() c.JSON() c.HTML()

从之前程序继续

package main

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

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.String(200, "%v", "Hello World")
	})
	r.Run()
	
}

在 r.GET 中,我们使用了 c.String ,类似地,还可以使用 c.JSON()

		c.JSON(200,gin.H{
			"success":true,
			"msg":"你好 gin",
		})

gin.H 其实就是 map[string]interface{} 的捷径,在里面写上键值对,就能成功访问,可以往里面放各种各样的类型或者结构体返回

而 c.HTML() 是用于渲染模板的(下节详细讲),首先新建一个 templates 文件夹,在其中新建 index.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>
    {{.time}}<br>
    {{.location}}<br>
    {{.weather}}
</body>
</html>

其中,后台的数据用双大括号阔上,加一个点,再写上名字

回到 main.go ,在新建路由的下一行加上这句话来加载模板

r.LoadHTMLGlob("templates/*")

然后再配置路由

c.HTML(200, "index.html", gin.H{
	"time":     "20xx年x月x日",
	"location": "xx市",
	"weather":  "晴",
})

运行,访问本地,可以看到已经正常渲染


P3:HTML 模板与静态文件服务

一、 模板配置

模板配置分为两种情况

1. 全部模板放在一个目录

在 templates 里放置所有模板,再在 main.go 里使用r.LoadHTMLGlob("templates/*")即可

2. 模板放在不同目录

这里以下面的结构为例子

test
│  go.mod
│  go.sum
│  main.go
│
└─templates
    ├─back
    │       index.html //后端页面
    └─front
            index.html //前端页面

在前端页面的开头加上{{ define "front/index.html" }},并在结尾加上{{ end }}

{{ define "front/index.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>
    <h1>这里是前端</h1>
</body>
</html>

{{ end }}

后端类似

{{ define "back/index.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>
    <h1>这里是后端</h1>
</body>
</html>

{{ end }}

这相当与给模板起一个名称,defineend 是成对出现的

回到 main.go ,加载模板的语句要变化,使用两个星号表示一层目录,并分别配置前后端路由

package main

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

func main() {
	r := gin.Default()

	// ** 表示一层目录
	r.LoadHTMLGlob("templates/**/*")

	// 前端路由
	r.GET("/front", func(c *gin.Context) {
		c.HTML(200, "front/index.html", gin.H{})
	})

	// 后端路由
	r.GET("/back", func(c *gin.Context) {
		c.HTML(200, "back/index.html", gin.H{})
	})

	r.Run()

}

当然,也可以选择用r.LoadHTMLFiles()来引用单个模板文件

二、模板语法

有关模板的语法很多

1、输出数据

模板语法都包含在{{}} 中间,其中{{.}} 中的点表示当前对象

当我们传入一个结构体对象时,我们可以根据.来访问结构体对于的字段

例如,在 main.go 中创建一个 UserInfo 类型

type UserInfo struct {
	Name   string
	Gander string
	Age    int
}

在前端页面中添加对应的字段

<p>{{.user.Name}}</p>
<p>{{.user.Gander}}</p>
<p>{{.user.Age}}</p>

实例化一个 user ,并传递

user := UserInfo{
	Name:   "张三",
	Gander: "男",
	Age:    18,
}

// 前端路由
r.GET("/front", func(c *gin.Context) {
	c.HTML(200, "front/index.html", gin.H{
		"user": user,
	})
})

2、解构结构体

在上面,我们使用这一段

<p>{{.user.Name}}</p>
<p>{{.user.Gander}}</p>
<p>{{.user.Age}}</p>

使用 with 可以解构结构体,简化这一步骤

{{with .user}}
    <p>{{.Name}}</p>
    <p>{{.Gander}}</p>
    <p>{{.Age}}</p>
{{end}}

3、注释

{{/* a comment */}}

注释不能嵌套,并且必须紧贴分界符始止

4、变量

可以在模板中声明变量,来保存传入模板的数据或其他语句生成的结果,方法如下:

<h4>{{$t := .title}}</h4>

<h4>{{$t}}</h4>

5、移除空格

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

{{- .Name -}}

6、比较大小

  • eq 等于 ( == )
  • ne 不等于 ( != )
  • lt 小于 ( < )
  • le 小于等于 ( <= )
  • gt 大于 ( > )
  • ge 大于等于 ( >= )

注意,使用 eq A B来比较 A B 是否相等,而不是A eq B 比较大小常和下面的判断一起使用

7、条件判断

  • if-else-end 结构
{{if  gt .score 60}}
    及格
{{else}}
    不及格
{{end}}
  • if-else if-else-end 结构
{{if gt .score 90}}
    优秀
{{else if gt .score 60}}
    及格
{{else}}
    不及格
{{end}}

8、遍历

Go的模板语法中使用 range 关键字进行遍历,有以下两种写法,其中 obj 必须是数组、切片、字典或者通道

{{range $key,$value := .obj}}
    {{$key}}:{{$value}}
{{end}}
{{range $key,$value := .obj}}
    {{$key}}:{{$value}}
{{else}}
    没有数据
{{end}}

如果 obj 为空,则会返回"没有数据"

例:

r.GET("/front", func(c *gin.Context) {
	c.HTML(200, "front/index.html", gin.H{
		"user":  user,
		"hobby": []string{},
	})
})

{{range $key,$value := .hobby}}
    {{$key}}:{{$value}}
{{else}}
    没有爱好
{{end}}

9、函数

函数有预定义的,还可以自行定义,但其实预定义的基本没什么用,所以一般都用自己定义的函数

  • 预定义函数 and 函数返回它的第一个 empty 参数或者最后一个参数; 就是说"and x y"等价于"if x then y else x";所有参数都会执行;

or 返回第一个非 empty 参数或者最后一个参数; 亦即"or x y"等价于"if x then x else y";所有参数都会执行;

not 返回它的单个参数的布尔值的否定

len 返回它的参数的整数类型长度

index 执行结果为第一个参数以剩下的参数为索引/键指向的值; 如"index x 1 2 3"返回 x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。

print 即 fmt.Sprint

printf 即 fmt.Sprintf

println 即 fmt.Sprintln

html 返回与其参数的文本表示形式等效的转义 HTML。 这个函数在 html/template 中不可用。

urlquery 以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。 这个函数在 html/template 中不可用。

js 返回与其参数的文本表示形式等效的转义 JavaScript。

call 执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数; 如"call .X.Y 1 2"等价于 go 语言里的 dot.X.Y(1, 2); 其中 Y 是函数类型的字段或者字典的值,或者其他类似情况; call 的第一个参数的执行结果必须是函数类型的值(和预定义函数如 print 明显不同); 该函数类型值必须有 1 到 2 个返回值,如果有 2 个则后一个必须是 error 接口类型; 如果有 2 个返回值的方法返回的 error 非 nil,模板执行会中断并返回给调用模板执行者该错误;

  • 自定义函数 例如,我们在后台有一个 UNIX 时间戳,希望在渲染模板时自动转换常用的时间格式
r.GET("/back", func(c *gin.Context) {
	c.HTML(200, "back/index.html", gin.H{
		"date":1642601992,
	})
})

首先需要自己在 main.go 里先写一个实现这一功能的函数

func Unix2Time(timestamp int)string{
	t:=time.Unix(int64(timestamp),0)
	return t.Format("2000-01-02 03:04:05")
}

然后在加载模板上方创建引擎下方通过r.SetFuncMap()注册自定义模板函数

r.SetFuncMap(template.FuncMap{
	"Unix2Time": Unix2Time,
})

现在,在模板中就可以调用函数了

{{Unix2Time .date}}

10、模板嵌套

比如,现在需要给前端和后端设计一个公共的标题 在 templates 中新建 public/page_header.html

{{ define "public/page_header.html" }}
    <h1>
        我是一个公共的标题
    </h1>
{{ end }}

使用{{template "public/page_header.html" .}}引入**(注意末尾的点)** 当然,嵌套同一个模板并不表示会显示相同的内容,比如我们稍加修改,就能分别展示前端和后端的标题

三、静态文件服务

当我们渲染的 HTML 文件中引用了静态文件时(如css、js、图片等),我们需要使用r.Static配置静态 web 服务

func main() {
    r := gin.Default()
    r.Static("/static", "./static")  
    //前面的 /static 表示路由(从外部访问) 后面的./static 表示本地路径
    r.LoadHTMLGlob("templates/**/*")
    // ... 
    r.Run(":8080")
}
<link rel="stylesheet" href="/static/css/base.css" />   

注意在模板里引用时 static 前面的斜杠不要漏


P4:路由传值与动态路由

一、GET 请求传值

GET请求就是在 URL 中携带的参数,如

http://127.0.0.1:8080/?username=admin&passwd=12345&page=10

就传递了usernamepasswordpage三个参数

r.GET("/", func(c *gin.Context) {
	username := c.Query("username")
	passwd := c.Query("passwd")
	page := c.DefaultQuery("page", "1")

	c.JSON(http.StatusOK, gin.H{
		"username": username,
		"passwd":   passwd,
		"page":     page,
	})
})

使用c.Query()接收数据,而使用c.DefaultQuery()还能指定默认值

二、POST 请求传值,获取 form 表单数据

我们现在需要配置两个路由,其中addUser.html需要写模板创建一个表单并向doAddUser.html发送POST请求,而doAddUser.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.html" method="post">
        用户名:<input type="text" name="username" /><br>
        密码:<input type="password" name="password" /><br>
        <input type="submit" value="提交">
    </form>
</body>
</html>
r.GET("addUser.html", func(c *gin.Context) {
	c.HTML(http.StatusOK, "addUser.html", gin.H{})
})

// POST 传值
r.POST("doAddUser.html", func(c *gin.Context) {
	username := c.PostForm("username")
	password := c.PostForm("password")
	age := c.DefaultPostForm("age", "18")

	c.JSON(http.StatusOK, gin.H{
		"username": username,
		"password": password,
		"age":      age,
	})
})

与 GET 类似也有两个函数,使用c.PostForm()接收数据,而c.DefaultPostForm()能指定默认值

三、动态路由传值

假设在/user目录下为每一个用户以 uid 分别提供页面,如/user/1/user/2/user/3

r.GET("/user/:uid", func(c *gin.Context) {
	uid := c.Param("uid")
	c.String(http.StatusOK, "这是第 %s 位用户的页面", uid)
})

四、解析 JSON 和 XML 数据

在 API 的开发中,我们经常会用到 JSON 或 XML 来作为数据交互的格式,这个时候我们 可以使用 GetRawData()+Unmarshal() 获取数据

<?xml version="1.0" encoding="UTF-8"?>
<article>
<content type="string">我是张三</content>
<title type="string">张三</title>
</article>
type Article struct {
	Title   string `xml:"title" json:"title"`  // tag 不要忘
	Content string `xml:"content" json:"content"`
}
r.POST("/xml", func(c *gin.Context) {
	article := &Article{}
	xmlSliceData, _ := c.GetRawData()
	// GetRawData 返回的是切片,再用 Unmarshal 转换至结构体
	if err := xml.Unmarshal(xmlSliceData, &article); err == nil {
		c.JSON(http.StatusOK, article)
	} else {
		c.JSON(http.StatusBadRequest, gin.H{
			"err": err.Error(),
		})
	}
})

五、绑定数据到结构体

更好的选择是使用基于请求的 Content-Type 识别请求数据类型并利用反射机制自动提取请求中QueryString、 form 表单、JSON、 XML等参数到结构体中

下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString型的数据,并把值绑定到指定的结构体对象

type UserInfo struct {
Username string `form:"username" json:"user"`    // tag 不要忘
Password string `form:"password" json:"password"` 
}
r.POST("doAddUser.html", func(c *gin.Context) {
	user := &UserInfo{}
	if err := c.ShouldBind(&user); err == nil { //解析到 user 结构体中
		c.JSON(http.StatusOK, user)
	} else {
		c.JSON(http.StatusOK, gin.H{
			"err": err.Error(),
		})
	}
})


P5:路由分组和路由文件抽离

在之前的方法中,配置路由、处理数据都在 main.go 文件中进行,当遇到稍大的项目时维护就会十分吃力,也难以团队协作,所以我们现在需要进行路由分组和文件抽离

package main

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

func main() {

	r := gin.Default()

	//前台路由
	r.GET("/", func(c *gin.Context) {
		c.String(200, "首页")
	})
	r.GET("/news", func(c *gin.Context) {
		c.String(200, "新闻")
	})

	//后台路由
	r.GET("/admin", func(c *gin.Context) {
		c.String(200, "后台首页")
	})
	r.GET("/admin/user", func(c *gin.Context) {
		c.String(200, "设置用户")
	})
	r.GET("/admin/article", func(c *gin.Context) {
		c.String(200, "新闻列表")
	})

	//api 路由
	r.GET("/api", func(c *gin.Context) {
		c.String(200, "我是一个api接口")
	})
	r.GET("/api/userlist", func(c *gin.Context) {
		c.String(200, "我是一个api接口--userlist")
	})
	r.GET("/api/plist", func(c *gin.Context) {
		c.String(200, "我是一个api接口--plist")
	})

	r.Run()

}

一、路由分组

以上面的代码为例,我们可以按照前台、后台和 api 接口三个模块来分组

package main

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

func main() {

	r := gin.Default()

	//前台路由
	defaultRouters := r.Group("/")
	{
		defaultRouters.GET("/", func(c *gin.Context) {
			c.String(200, "首页")
		})
		defaultRouters.GET("/news", func(c *gin.Context) {
			c.String(200, "新闻")
		})
	}

	//后台路由
	adminRouters := r.Group("/admin")
	{
		adminRouters.GET("/", func(c *gin.Context) {
			c.String(200, "后台首页")
		})
		adminRouters.GET("/user", func(c *gin.Context) {
			c.String(200, "设置用户")
		})
		adminRouters.GET("/article", func(c *gin.Context) {
			c.String(200, "新闻列表")
		})
	}

	//api 路由
	apiRouters := r.Group("/api")
	{
		apiRouters.GET("/", func(c *gin.Context) {
			c.String(200, "我是一个api接口")
		})
		apiRouters.GET("/userlist", func(c *gin.Context) {
			c.String(200, "我是一个api接口--userlist")
		})
		apiRouters.GET("/plist", func(c *gin.Context) {
			c.String(200, "我是一个api接口--plist")
		})
	}

	r.Run()

}

二、路由文件抽离

接下来继续抽离,把路由组抽离到其他文件中

新建routers文件夹,并依照3个路由组新建 go 文件

test
│  go.mod
│  go.sum
│  main.go
│
└─routers
        adminRouters.go
        apiRouters.go
        defaultRouters.go

文件内容以defaultRouters.go为例,注意函数开头大写,因为要在main.go中调用

package routers

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

func DefaultRoutersInit(r *gin.Engine) {
	//前台路由
	defaultRouters := r.Group("/")
	{
		defaultRouters.GET("/", func(c *gin.Context) {
			c.String(200, "首页")
		})
		defaultRouters.GET("/news", func(c *gin.Context) {
			c.String(200, "新闻")
		})
	}
}

现在,只需在main.go中调用 routers 包中的函数即可

package main

import (
	"test/routers"

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

func main() {

	r := gin.Default()

	//前台路由
	routers.DefaultRoutersInit(r)

	//后台路由
	routers.AdminRoutersInit(r)

	//api 路由
	routers.ApiRoutersInit(r)

	r.Run()

}


P6:自定义控制器和控制器的继承

自定义控制器

在对路由分组后,我们对控制器也可以进行分组

首先新建controllers文件夹,然后在其中按业务逻辑创建控制器

TEST
│  go.mod
│  go.sum
│  main.go
│
├─controllers
│  ├─admin
│  │      adminController.go
│  │
│  ├─api
│  │      apiController.go
│  │
│  └─homePage
│          homePageController.go //因为default是关键字
│
└─routers
        adminRouters.go
        apiRouters.go
        defaultRouters.go

/controllers/admin/adminController.go为例,编辑控制器

package admin

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

type AdminController struct {
}

func (c AdminController) Index(con *gin.Context) {
	con.String(200, "后台首页")
}

func (c AdminController) User(con *gin.Context) {
	con.String(200, "设置用户")
}

func (c AdminController) Article(con *gin.Context) {
	con.String(200, "新闻列表")
}

再去到routers/adminRouters.go中重新配置路由

package routers

import (
	"test/controllers/admin"

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

func AdminRoutersInit(r *gin.Engine) {
	//后台路由
	adminRouters := r.Group("/admin")
	{
		adminRouters.GET("/", admin.AdminController{}.Index)
		adminRouters.GET("/user", admin.AdminController{}.User)
		adminRouters.GET("/article", admin.AdminController{}.Article)
	}
}

注意不是admin.AdminController{}.Index(),这表示执行这个方法,而不是绑定到这个方法上

控制器的继承

上面之所以选择加一个结构体然后用它的方法,不是多此一举的,就是利用结构体的特性来继承父结构体的方法

这里定义两个全局的方法,用于返回成功和失败的消息

新建\controllers\admin\baseController.go

package admin

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

type BaseController struct {
}

func (c BaseController) Success(con *gin.Context) {
	con.String(200, "成功")
}
func (c BaseController) Error(con *gin.Context) {
	con.String(200, "失败")
}

再在adminController.go中嵌套BaseController struct,并继承和调用它的方法

package admin

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

type AdminController struct {
	BaseController
}

func (c AdminController) Index(con *gin.Context) {
	con.String(200, "后台首页")
}

func (c AdminController) User(con *gin.Context) {
	c.Success(con)
}

func (c AdminController) Article(con *gin.Context) {
	con.String(200, "新闻列表")
}

可以看见控制器已经成功继承


P7:路由中间件

初识中间件

从最简单的模板开始

package main

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

func main() {

	r := gin.Default()

	r.GET("/", func(c *gin.Context) {
		c.String(200, "测试页面")
	})

	r.Run()

}

在这个例子中,只有一个函数处理根目录下的路由

然而其实也可以传递多个函数,它们将被依次执行,最后一个函数前面触发的方法都可以称为中间件

中间件适合处理一些公共的业务逻辑,比如登录认证权限校验数据分页记录日志耗时统计

package main

import (
	"fmt"

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

func initMiddleware(ctx *gin.Context) {
	fmt.Println("我是一个中间件")
}

func main() {

	r := gin.Default()

	r.GET("/", initMiddleware, func(c *gin.Context) {
		c.String(200, "测试页面")
	})

	r.Run()

}

可以看见,在处理路由时,先执行了中间件,再响应了页面

.Next()方法

在中间件中调用.Next()方法可以递归调用下一个中间件或最终函数

例1:统计一个请求的执行时间

package main

import (
	"fmt"
	"time"
    
	"github.com/gin-gonic/gin"
)

func initMiddleware(ctx *gin.Context) {
	start := time.Now().UnixNano()
	ctx.Next()
	end := time.Now().UnixNano()
	fmt.Println(end - start)
}

func main() {

	r := gin.Default()

	r.GET("/", initMiddleware, func(c *gin.Context) {
		time.Sleep(time.Second) //休眠1秒
		c.String(200, "测试页面")
	})

	r.Run()

}

例2:多个中间件的递归执行

package main

import (
	"fmt"
    
	"github.com/gin-gonic/gin"
)

func MiddlewareOne(ctx *gin.Context) {
	fmt.Println("第一个中间件开始")
	ctx.Next()
	fmt.Println("第一个中间件结束")
}

func MiddlewareTwo(ctx *gin.Context) {
	fmt.Println("第二个中间件开始")
	ctx.Next()
	fmt.Println("第二个中间件结束")
}

func main() {

	r := gin.Default()

	r.GET("/", MiddlewareOne, MiddlewareTwo, func(c *gin.Context) {
		c.String(200, "测试页面")
	})

	r.Run()

}

.Abort()方法

在中间件中调用.Next()方法可以终止该请求的剩余处理程序

package main

import (
	"fmt"

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

func MiddlewareOne(ctx *gin.Context) {
	fmt.Println("第一个中间件开始")
	ctx.Abort()
	fmt.Println("第一个中间件结束")
}

func MiddlewareTwo(ctx *gin.Context) {
	fmt.Println("第二个中间件开始")
	ctx.Next()
	fmt.Println("第二个中间件结束")
}

func main() {

	r := gin.Default()

	r.GET("/", MiddlewareOne, MiddlewareTwo, func(c *gin.Context) {
		c.String(200, "测试页面")
	})

	r.Run()

}

全局中间件

在引擎上调用.Use()方法可以配置全局中间件

package main

import (
	"fmt"
    
	"github.com/gin-gonic/gin"
)

func initMiddleware(ctx *gin.Context) {
	fmt.Println("我是一个中间件")
}

func main() {

	r := gin.Default()

	r.Use(initMiddleware) //配置全局中间件

	r.GET("/test1", func(c *gin.Context) {
		c.String(200, "测试页面1")
	})

	r.GET("/test2", func(c *gin.Context) {
		c.String(200, "测试页面2")
	})
	r.Run()

}

在路由分组中配置中间件

还记得之前的路由分组吗?

\routers\adminRouters.go中,我们之前是这样写的

package routers

import (
	"test/controllers/admin"

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

func AdminRoutersInit(r *gin.Engine) {
	//后台路由
	adminRouters := r.Group("/admin")
	{
		adminRouters.GET("/", admin.AdminController{}.Index)
		adminRouters.GET("/user", admin.AdminController{}.User)
		adminRouters.GET("/article", admin.AdminController{}.Article)
	}
}

现在,需要为adminRouters这个路由组配置中间件,一共有两种写法

写法1:

func initMiddleware(ctx *gin.Context) {
	fmt.Println("我是一个中间件")
}

func AdminRoutersInit(r *gin.Engine) {
	//后台路由
	adminRouters := r.Group("/admin", initMiddleware) //配置中间件
	{
		adminRouters.GET("/", admin.AdminController{}.Index)
		adminRouters.GET("/user", admin.AdminController{}.User)
		adminRouters.GET("/article", admin.AdminController{}.Article)
	}
}

写法2:

func initMiddleware(ctx *gin.Context) {
	fmt.Println("我是一个中间件")
}

func AdminRoutersInit(r *gin.Engine) {
	//后台路由
	adminRouters := r.Group("/admin")
	adminRouters.Use(initMiddleware) //配置中间件
	{
		adminRouters.GET("/", admin.AdminController{}.Index)
		adminRouters.GET("/user", admin.AdminController{}.User)
		adminRouters.GET("/article", admin.AdminController{}.Article)
	}
}

中间件和对应控制器之间共享数据

在中间件中可以设置键值对(.Set()方法),供其他中间件或控制器读取(.Get()方法)

\routers\adminRouters.go

package routers

import (
	"fmt"
	"test/controllers/admin"

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

func initMiddleware(ctx *gin.Context) {
	fmt.Println("我是一个中间件")
	ctx.Set("username", "张三") //设置数据
}

func AdminRoutersInit(r *gin.Engine) {
	//后台路由
	adminRouters := r.Group("/admin")
	adminRouters.Use(initMiddleware)
	{
		adminRouters.GET("/", admin.AdminController{}.Index)
		adminRouters.GET("/user", admin.AdminController{}.User)
		adminRouters.GET("/article", admin.AdminController{}.Article)
	}
}

\controllers\admin\adminController.go

package admin

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

type AdminController struct {
	BaseController
}

func (c AdminController) Index(con *gin.Context) {
	con.String(200, "后台首页")
}

func (c AdminController) User(con *gin.Context) {
	username, _ := con.Get("username") //获取数据
	con.String(200, username.(string))
}

func (c AdminController) Article(con *gin.Context) {
	con.String(200, "新闻列表")
}

中间件注意事项

默认中间件

gin.Default()默认使用了 LoggerRecovery 中间件

  • Logger 中间件将日志写入 gin.DefaultWriter,即使配置了 GIN_MODE=release
  • Recovery 中间件会 recover 任何 panic,如果有 panic 的话,会写入 500 响应码

如果不想使用上面两个默认的中间件,可以使用 gin.New()新建一个没有任何默认中间件的路由

中间件中使用 goroutine

当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(c *gin.Context), 必须使用其只读副本(c.Copy())

func initMiddleware(ctx *gin.Context) {
	ctxCp:=ctx.Copy()
	go func ()  {
		...
	}
}

P8:自定义 Model

关于 Model

如果我们的应用非常简单的话,我们可以在 Controller 里面处理常见的业务逻辑。但是如果我们 有一个功能想在多个控制器、或者多个模板里面复用 的话,那么我们就可以把公共的功能单独抽取出来作为一个模块(Model)

封装一个 Model

新建 models/tools.go,并在里面实现一个Unix时间戳转日期时间的功能

package models

import (
	"time"
)

func UnixToDate(timestamp int) string {
	t := time.Unix(int64(timestamp), 0)
	return t.Format("2006-01-02 15:04:05")
}

调用 Model

在控制器中调用

\controllers\admin\adminController.go为例

func (c AdminController) Index(con *gin.Context) {
	date := models.UnixToDate(1646554975)
	con.String(200, "转换后的日期和时间是:"+date)
}

在模板文件中调用

注意顺序,注册模板函数需要在加载模板上面

main.go

package main

import (
	"html/template"
	"test/models"
	"test/routers"

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

func main() {

	r := gin.Default()

	r.SetFuncMap(template.FuncMap{
		"unixToDate": models.UnixToDate,
	})

	r.LoadHTMLGlob("templates/**/*")

	//前台路由
	routers.DefaultRoutersInit(r)
	//后台路由
	routers.AdminRoutersInit(r)
	//api 路由
	routers.ApiRoutersInit(r)

	r.Run()
}

\controllers\admin\adminController.go

package admin

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

type AdminController struct {
	BaseController
}

func (c AdminController) Index(con *gin.Context) {
	con.HTML(200, "admin/index.html", gin.H{
		"now": 1646554975,
	})
}

func (c AdminController) User(con *gin.Context) {
	username, _ := con.Get("username")
	con.String(200, username.(string))
}

func (c AdminController) Article(con *gin.Context) {
	con.String(200, "新闻列表")
}

\templates\admin\index.html

{{ define "admin/index.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>
    <h2>{{.now | unixToDate}}</h2>
</body>
</html>

{{ end }}


P9:文件上传

单文件上传

定义模板

<body>
    <h2>文件上传</h2>
    <form action="/admin/doUpload" method="post" enctype="multipart/form-data">
        用户名: <input type="text" name="username" placeholder="用户名"> <br> <br>
        文  件:<input type="file" name="file"><br> <br>
        <input type="submit" value="提交">
    </form>
</body>

注意:enctype="multipart/form-data"不能漏

添加控制器

func (c AdminController) DoUpload(con *gin.Context) {
	username := con.PostForm("username")
	file, err := con.FormFile("file")

	if err != nil {
		con.JSON(http.StatusInternalServerError, gin.H{
			"message": err.Error(),
		})
		return
	}

	dst := path.Join("./static/upload", file.Filename)
	fmt.Println(dst)
	con.SaveUploadedFile(file, dst)
	con.JSON(http.StatusOK, gin.H{
		"message":  fmt.Sprintf("'%s',uploaded!", file.Filename),
		"username": username,
	})
}

文件的保存文件夹应当提前创建好

配置路由

adminRouters.POST("/doUpload", admin.AdminController{}.DoUpload)


多文件上传

对于多文件,可以拆成多个单文件分别手动处理,也可以按照如下的方式

模板

<body>
    <h2>文件上传</h2>
    <form action="/admin/doUpload" method="post" enctype="multipart/form-data">
        用户名: <input type="text" name="username" placeholder="用户名"> <br> <br>
        文  件1:<input type="file" name="file[]"><br> <br>
        文  件2:<input type="file" name="file[]"><br> <br>
        <input type="submit" value="提交">
    </form>
</body>

控制器

func (c AdminController) DoUpload(con *gin.Context) {
	username := con.PostForm("username")

	form, _ := con.MultipartForm()
	files := form.File["file[]"]

	for _, file := range files {
		dst := path.Join("./static/upload", file.Filename)
		con.SaveUploadedFile(file, dst)
	}

	con.JSON(http.StatusOK, gin.H{
		"message":  "文件上传成功",
		"username": username,
	})

}


检查后缀并按日期保存

在上面的例子中,保存文件时文件名都没有修改,这将带来两个缺点:

  • 太乱了
  • 同名文件会被覆盖

于是,按时间保存就非常有必要,下面将以上传图片为例来演示

总体思路:

  1. 获取上传的文件
  2. 获取后缀名,判断是否是图片
  3. 创建图片保存目录
  4. 生成文件名称
  5. 保存文件

首先在tools.go中添加两个全局方法

func GetUnix() int64 {
	return time.Now().Unix()
}

func GetDay() string {
	template := "20060102"
	return time.Now().Format(template)
}

adminController.go

func (c AdminController) DoUpload(con *gin.Context) {
	username := con.PostForm("username")

	//1获取上传的文件
	file, _ := con.FormFile("file")

	//2获取后缀名,判断是否是图片
	extName := path.Ext(file.Filename)
	allowExtMap := map[string]bool{
		".jpg":  true,
		".png":  true,
		".gif":  true,
		".jpge": true,
	}

	if _, ok := allowExtMap[extName]; !ok {
		con.String(http.StatusOK, "文件类型不合法")
		return
	}

	//3创建图片保存目录
	day := models.GetDay()
	dir := "./static/upload/" + day + "/"
	if err := os.MkdirAll(dir, 0666); err != nil {
		fmt.Println(err)
		return
	}

	//4生成文件名称
	filename := strconv.FormatInt(models.GetUnix(), 10) + extName
	
    //5保存文件
	con.SaveUploadedFile(file, dir+filename)

	con.JSON(http.StatusOK, gin.H{
		"message":  "文件上传成功",
		"username": username,
	})
}


P10:Cookie 与 Session

初识 Cookie

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

cookie 存储于访问者计算机的浏览器中,可以让我们用同一个浏览器访问同一个域名的时候共享数据

Cookie 能实现的简单功能:

  • 保持用户登录状态

  • 保存用户浏览记录


使用 Cookie

设置 Cookie

con.SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool)
  • maxAge:过期时间

    • 大于0,设置过期时间,单位为秒
    • 小于0,删除本Cookie
    • 等于0,设置为当关闭浏览器时过期
  • pathCookie的路径

  • domain:作用域,若要在多个二级域名中使用,如a.example.comb.example.com,则要写成.example.com

  • secure:为True时,仅在HTTPS中生效

  • httpOnly:用于防止客户端脚本通过document.cookie属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改

读取 Cookie

cookie, err := con.Cookie("name")

样例

defaultRouters.go

package routers

import (
	"test/controllers/homePage"

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

func DefaultRoutersInit(r *gin.Engine) {
	//前台路由
	defaultRouters := r.Group("/")
	{
		defaultRouters.GET("/", homePage.HomePageController{}.Index)
		defaultRouters.GET("/news", homePage.HomePageController{}.News)
		defaultRouters.GET("/user", homePage.HomePageController{}.User)
		defaultRouters.GET("/login", homePage.HomePageController{}.Login)
		defaultRouters.GET("/logout", homePage.HomePageController{}.Logout)
	}
}

homePageController.go

package homePage

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

type HomePageController struct {
}

func (c HomePageController) Login(con *gin.Context) {
	con.SetCookie("username", "张三", 3600, "/", "127.0.0.1", false, true)
	con.String(200, "已登录")
}

func (c HomePageController) Logout(con *gin.Context) {
	username, _ := con.Cookie("username")
	con.SetCookie("username", username, -1, "/", "127.0.0.1", false, true)
}

func (c HomePageController) User(con *gin.Context) {
	username, _ := con.Cookie("username")
	con.String(200, "用户:"+username)
}

func (c HomePageController) Index(con *gin.Context) {
	con.String(200, "首页")
}

func (c HomePageController) News(con *gin.Context) {
	con.String(200, "新闻")
}


初识 Session

Session技术与Cookie类似,最大的不同是Cookie是存储在客户端的,而Session是存储在服务端的

当客户端浏览器第一次访问服务器并发送请求时,服务器端会创建一个 session 对象,生成 一个类似于 key,value 的键值对,然后将 value 保存到服务器 将 key(cookie)返回到浏览器(客户端)。浏览器下次访问时会携带 key(cookie),找到对应的 session(value)

安装 session 包

gin是不集成session的,只能用第三方的

go get github.com/gin-contrib/sessions

样例

main.go

package main

import (
	"html/template"
	"test/models"
	"test/routers"

	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
)

func main() {

	r := gin.Default()

	r.SetFuncMap(template.FuncMap{
		"unixToDate": models.UnixToDate,
	})

	r.LoadHTMLGlob("templates/**/*")

	store := cookie.NewStore([]byte("123456"))
	r.Use(sessions.Sessions("mysession", store))

	//前台路由
	routers.DefaultRoutersInit(r)
	//后台路由
	routers.AdminRoutersInit(r)
	//api 路由
	routers.ApiRoutersInit(r)

	r.Run()
}

homePageController.go

package homePage

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
)

type HomePageController struct {
}

func (c HomePageController) Login(con *gin.Context) {
	// con.SetCookie("username", "张三", 0, "/", "127.0.0.1", false, true)
	session := sessions.Default(con)
	session.Options(sessions.Options{
		MaxAge: 3600 * 6, //6h
	})
	session.Set("username", "张三")
	session.Save()
	con.String(200, "已登录")
}

func (c HomePageController) Logout(con *gin.Context) {
	session := sessions.Default(con)
	session.Clear()
	session.Save()
}

func (c HomePageController) User(con *gin.Context) {
	session := sessions.Default(con)
	username := session.Get("username")
	con.JSON(200, gin.H{
		"username": username,
	})
}

func (c HomePageController) Index(con *gin.Context) {
	con.String(200, "首页")
}

func (c HomePageController) News(con *gin.Context) {
	con.String(200, "新闻")
}