这是我参与「第三届青训营 -后端场」笔记创作活动的第 6 篇笔记
因为大项目里要用到 Gin 框架,而这东西我已经有点忘了,故特来复习
P1:环境搭建、简单的路由配置
一、Gin 是什么
Gin 是一个用 Go (Golang) 编写的 web 框架。 它是一个类似于 martini 但拥有更好性能的 API 框架, 由于 httprouter,速度提高了近 40 倍。
同时,它目前在 GitHub 上已经有了 50k+ 的 Star,可谓是非常热门
二、环境搭建
注意:以下步骤需要全程魔法上网
-
在 VScode 中安装 Go 扩展
-
新建 test 文件夹,并在其中新建一个 main.go
这时会提醒你下载很多工具,确认下载即可,这里就看你的网速了,记得魔法上网
- 终端里切换到 test 目录,并执行下面的命令
go mod init test
go get -u github.com/gin-gonic/gin
- 新建 main.go ,并输入
import "github.com/gin-gonic/gin"
- 再在终端中执行命令
go mod tidy
这一步执行完毕后,目录中应该会有 go.mod 和 go.sum 两个文件
- 用下面的代码测试一下
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 }}
这相当与给模板起一个名称,define 和 end 是成对出现的
回到 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
就传递了username,password和page三个参数
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()默认使用了 Logger 和 Recovery 中间件
Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=releaseRecovery中间件会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,
})
}
检查后缀并按日期保存
在上面的例子中,保存文件时文件名都没有修改,这将带来两个缺点:
- 太乱了
- 同名文件会被覆盖
于是,按时间保存就非常有必要,下面将以上传图片为例来演示
总体思路:
- 获取上传的文件
- 获取后缀名,判断是否是图片
- 创建图片保存目录
- 生成文件名称
- 保存文件
首先在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,设置为当关闭浏览器时过期
- 大于
-
path:Cookie的路径 -
domain:作用域,若要在多个二级域名中使用,如a.example.com和b.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, "新闻")
}