用自己的话表达学到的东西是为了节约时间。
别把窘境,迁怒于别人,唯一可以抱怨的,只是不够努力的自己。
0. 了解Gin框架
安装
go get -u github.com/gin-gonic/gin
使用
import "github.com/gin-gonic/gin"
例子:编写HelloWorld 准备工作: 开启go module:
GOPROXY=https://goproxy.cn,direct
然后
go mod init GinWeb
go mod tidy
编写HelloWorld。 先创建GinWeb/gindemo/helloworld.go,代码如下: 编程目的:使用 gin.Default() 和Run方法
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main(){
router := gin.Default()
router.GET("/",func(c *gin.Context){
c.String(http.StatusOK,"Hello World \n")
})
router.Run(":8000")
}
这样就实现了Gin实现HelloWorld,创建了一个router(路由), 然后执行了Run方法。 右击运行程序,使用postman访问指定的ip和端口:http://127.0.0.1:8000 或者我们在cmd终端通过curl命令访问,输入:
curl http://127.0.0.1:8000
分析代码
1.router := gin.Default()
这是默认的服务器。
使用gin的Default方法创建一个路由Handle。
2.然后通过HTTP方法GET绑定路由规则和路由函数。
不同于net/http的路由函数,gin进行了封装,把request和response都封装到了gin.Context的上下文环境中
3.最后启动路由的方法监听端口,还可以使用http.ListenAndServe(":8000",router)
要知道一次请求处理的大体流程,只要能找到web框架的入口即可。 点击Run方法,可以看到关键的ListenAndServer, Engine结构体实现了ServeHTTP接口。 入口就是Engine实现的ServeHTTP接口。 几行代码就能实现web服务。 使用gin的Default方法创建了一个路由handler, 然后通过HTTP方法绑定路由规则和路由函数, 不同于net/http库的路由函数,gin进行了封装, 把request和response都封装到了gin.Context的上下文环境中, 最后是启动路由Run方法监听端口。
Gin API文档
继续分析:
1. Gin-Router
1.1 默认服务器
router.Run()
1.2 Http服务器
除了使用默认的服务器中的router.Run方法, 还可以使用http.ListenAndServe 方法。 编程目的:使用http.ListenAndServe 方法
func main(){
router := gin.Default()
router.GET("/",func(c *gin.Context){
c.String(http.StatusOK,"Hello CSDN!")
})
http.ListenAndServe(":8080",router)
}
测试效果:
1.3 路由
1.3.1 基本路由
基本路由gin框架采用的路由库是httprouter。
router := gin.Default()
router.GET("/someGet",getting)
router.POST("somePost",posting)
router.PUT("/somePut",putting)
router.PATCH("/somePatch",patching)
router.DELETE("/someDelete",deleting)
1.3.2 路由参数
gin不支持路由的正则表达式
1.3.2.1 API参数
router.GET("/user/:name",func(c *gin.Context){
name := c.Param("name")
c.String(http.StatusOK,name)
})
运行后postman输入 http://127.0.0.1:8000/user/keegan 结果: 冒号: 加上一个参数名组成路由参数。可以使用c.Param的方法读取其值。获取的是string。 比如/user/yjg可以匹配,而/user/和/user/yjg/不会被匹配。 代码: 编程目的:使用 c.Param 方法
func main(){
router := gin.Default()
router.GET("/user/:name",func(c *gin.Context){
name:= c.Param("name")
c.JSON(http.StatusOK,gin.H{
"name":name,
})
})
http.ListenAndServe(":8080",router)
}
结果:
除了: ,gin还提供了* 号处理参数,* 号能匹配的规则就更多。 代码: 编程目的:使用 c.Param 方法
func main(){
router := gin.Default()
router.GET("/user/:name/*action",func(c *gin.Context){
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK,message)
})
http.ListenAndServe(":8080",router)
}
结果:
1.3.2.2 URL参数
客户端向服务器发送请求,除了路由参数,其他的参数无非两种,查询字符串query string和报文体body参数。 所谓query string,即路由用? 以后连接的key1=value2&key2=value2 的形式的参数。当然这个key-value是经过urlencode编码。 URL 参数通过 DefaultQuery 或 Query 方法获取。
对于参数的处理,经常会出现参数不存在的情况,对于是否提供默认值,gin也考虑了,并且给出了一个优雅的方案, 使用c.DefaultQuery方法读取参数,其中当参数不存在的时候,提供一个默认值。 使用c.Query方法读取正常参数,当参数不存在的时候,返回空字串。 代码: 编程目的:使用 c.Query和c.DefaultQuery 方法
func main(){
router := gin.Default()
router.GET("/user",func(c *gin.Context){
nickname:= c.Query("nickname")
name := c.DefaultQuery("name","anonymous")
c.JSON(http.StatusOK,gin.H{
"name":name,
"nickname":nickname,
})
})
http.ListenAndServe(":8080",router)
}
结果: 一个字符一个字符地边看边输入,有效降低错误。当然还要明确练习的目标。
1.3.2.3 表单参数
了解表单参数是什么: http的报文体传输数据 常见的格式 有四种:
application/json —— 前后端数据传输的格式为json
application/x-www-form-urlencoded —— 把query string的内容,放到了body体里,需要 urlencode(解决中文乱码)
application/xml —— 前后端数据传输的格式为xml
multipart/form-data —— 用于图片上传
使用: 编程目的:表单参数通过 PostForm 方法获取 代码:
func main(){
router := gin.Default()
router.POST("/loginForm",func(c *gin.Context){
type1 := c.DefaultPostForm("type","alert")
username := c.PostForm("username")
password := c.PostForm("password")
hobbys := c.PostFormArray("bobby")
c.JSON(http.StatusOK,gin.H{
"type":type1,
"username":username,
"password":password,
"hobby":hobbys,
})
})
http.ListenAndServe(":8080",router)
}
我们还需要提供一个html页面(login.html),来进行post请求:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="http://127.0.0.1:8080/loginForm" method="post" enctype="application/x-www-form-urlencoded">
用户名:<input type="text" name="username">
<br>
密   码:<input type="password" name="password">
<br>
兴   趣:
<input type="checkbox" value="girl" name="hobby">金钱
<input type="checkbox" value="game" name="hobby">金钱
<input type="checkbox" value="money" name="hobby">金钱
<br>
<input type="submit" value="登录">
</form>
</body>
</html>
注意前台的请求接口地址是:http://127.0.0.1:8080/loginForm 后台要设计的一样。 然后先运行后台程序, 然后在浏览器运行login.html文件, 结果如下: 分析: username和password数据我们可以获取,type获取不到就使用默认值alert。 使用PostForm形式,注意必须要设置Post的type,同时此方法中忽略URL中带的参数,所有的参数需要从Body中获得(html body标签)。
1.3.2.4 文件上传
1.3.2.4.1 上传单个文件
表单参数回顾: multipart/form-data —— 用于图片上传 Windows查看端口—关闭端口占用
netstat -aon|findstr "8000"
taskkill /PID 8000 /F
管理员身份运行:
问题: 解决:这两个go文件用文件夹隔离。
使用gin实现文件上传。 先创建go文件:
func main(){
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
file, _ := c.FormFile("file")
log.Println(file.Filename)
c.SaveUploadedFile(file, file.Filename)
c.JSON(http.StatusOK, gin.H{
"uploaded": file.Filename,
})
})
router.Run(":8080")
}
再创建file.html页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传</title>
</head>
<body>
<form action="http://127.0.0.1:8080/upload" method="post" enctype="multipart/form-data">
头像:
<input type="file" name="file">
<br>
<input type="submit" value="提交">
</form>
</body>
</html>
结果: 运行程序后,打开浏览器选择文件—提交
1.3.2.4.2 上传多个文件
了解 上传多个文件: 所谓多个文件,无非就是多一次遍历文件,然后一次copy数据存储即可。
使用 c.Request.MultipartForm得到文件句柄,再获取文件数据,然后遍历读写。
go文件代码:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func main(){
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
form,err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest,gin.H{
"get form err":err.Error(),
})
}
files := form.File["files"]
for _,file := range files {
if err := c.SaveUploadedFile(file,file.Filename);err != nil{
c.String(http.StatusBadRequest,fmt.Sprintf("upload file err:%s\n",err.Error()))
return
}
}
c.String(http.StatusOK,fmt.Sprintf("Upload successfully %d files",len(files)))
})
router.Run(":8080")
}
files.html文件代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件s</title>
</head>
<body>
<h1>上传多个文件</h1>
<form action="http://127.0.0.1:8080/upload" method="post" enctype="multipart/form-data">
Files: <input type="file" name="files" multiple><br><br>
<input type="submit" value="提交">
</form>
</body>
</html>
结果:
1.3.2.5 路由分组
了解路由分组: 是方便管理一部分相同的URL。
用到了:
c.DefaultQuery
URL请求参数。 代码:
func main(){
router := gin.Default()
v1 := router.Group("/v1")
{
v1.GET("/login",loginEndpoint)
v1.GET("/submit",submitEndpoint)
v1.POST("/read",readEndpoint)
}
router.Run(":8080")
}
func loginEndpoint(c *gin.Context){
name := c.DefaultQuery("name","Guest")
c.String(http.StatusOK,fmt.Sprintf("Hello %s \n",name))
}
func submitEndpoint(c *gin.Context){
name := c.DefaultQuery("name","Guest")
c.String(http.StatusOK,fmt.Sprintf("Hello %s \n",name))
}
func readEndpoint(c *gin.Context){
name := c.DefaultQuery("name","Guest")
c.String(http.StatusOK,fmt.Sprintf("Hello %s \n",name))
}
结果:
2. Gin-Model
2.1 数据解析绑定
目前Gin支持JSON、XML、YAML和标准表单值的绑定。简单来说,,就是根据Body数据类型,将数据赋值到指定的结构体变量中 (类似于序列化和反序列化) 。
2.2 JSON绑定
了解JSON绑定: JSON的绑定 就是将request中的Body中的数据按照JSON格式进行解析,解析后存储到结构体对象中。 代码:
type Login struct {
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
func main() {
router := gin.Default()
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if json.User != "CSDN" || json.Password != "ZXCzxc123"{
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
router.Run("192.168.8.102:8082")
}
在centos7虚拟机中运行:
curl -v -X POST http://192.168.8.102:8082/loginJSON -H 'content-type:application/json' -d '{"user":"CSDN","password":"ZXCzxc123"}'
主力开发还得在linux上开发。 Windows 的 cmd 就不行:
2.3 Form表单
了解Form表单: 就是将c中的request中的body数据解析到form中。首先我们先看一下绑定普通表单的例子。 编写go文件: 【注意提交的接口地址是"http://192.168.8.102:8082/loginForm】
type Login struct {
UserName string `form:"username" json:"username" xml:"username" binding:"required"`
PassWord string `form:"password" json:"password" xml:"password" binding:"required"`
}
func main(){
router := gin.Default()
router.POST("/loginForm",func(c *gin.Context){
var form Login
if err := c.Bind(&form);err!=nil{
c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
return
}
if form.UserName != "CSDN" || form.PassWord != "ZXCzxc123"{
c.JSON(http.StatusUnauthorized,gin.H{
"status":"unauthorized",
})
return
}
c.JSON(http.StatusOK,gin.H{
"status":"you are logger in",
})
})
router.Run("192.168.8.102:8082")
}
编写login.html文件: 【注意提交的接口地址是"http://192.168.8.102:8082/loginForm】
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="http://192.168.8.102:8082/loginForm" method="post" enctype="application/x-www-form-urlencoded">
用户名:<input type="text" name="username">
<br>
密   码:<input type="password" name="password">
<br>
兴   趣:
<input type="checkbox" value="girl" name="hobby">金钱
<input type="checkbox" value="game" name="hobby">金钱
<input type="checkbox" value="money" name="hobby">金钱
<br>
<input type="submit" value="登录">
</form>
</body>
</html>
启动go程序;
运行login.html代码;
结果:
2.4 URI绑定
代码:
type Login struct {
UserName string `form:"username" json:"username" uri:"username" xml:"username" binding:"required"`
PassWord string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main(){
router := gin.Default()
router.GET("/:username/:password",func(c *gin.Context){
var login Login
if err := c.ShouldBindUri(&login);err!=nil{
c.JSON(http.StatusBadRequest,gin.H{
"msg":err,
})
return
}
c.JSON(http.StatusOK,gin.H{
"username":login.UserName,
"password":login.PassWord,
})
})
router.Run("192.168.8.102:8081")
}
结果: 如果是这样写的,那么结果是: uri参数名称不要写错了。根据事先设计好的参数名称来写代码。
3. 响应
3.1 JSON/XML/YAML渲染
代码:
type Message struct {
Name string `json:"user"`
Message string
Number int
}
func main(){
r := gin.Default()
r.GET("/someJSON",func(c *gin.Context){
c.JSON(http.StatusOK,gin.H{
"message":"CSDN代码写注释",
"status":http.StatusOK,
})
})
r.GET("/moreJSON",func(c *gin.Context){
msg := Message{"CSDN","代码写注释",9527}
c.JSON(http.StatusOK,msg)
})
r.GET("/someXML",func(c *gin.Context){
c.XML(http.StatusOK,gin.H{
"user":"CSDN",
"message":"代码写注释",
"status":http.StatusOK,
})
})
r.GET("/someYAML",func(c *gin.Context){
c.YAML(http.StatusOK,gin.H{
"message":"CSDN-代码写注释",
"status":http.StatusOK,
})
})
r.GET("/someProtoBuf",func(c *gin.Context){
resp := []int64{int64(1),int64(2)}
label := "test_protobuf"
data := &protoexample.Test{
Label:&label,
Reps: resp,
}
c.ProtoBuf(http.StatusOK,data)
})
r.Run(":8081")
}
结果:
3.2 HTML模板渲染
gin支持加载HTML模板, 然后根据模板参数进行配置并返回相应的数据。
func main(){
router := gin.Default()
router.LoadHTMLGlob("templates/*")
router.GET("/index",func(c *gin.Context){
// 根据完整的文件名渲染模板,并传递参数
c.HTML(http.StatusOK,"index.tmpl",gin.H{
"title":"为CSDN代码写注释点赞",
})
})
router.Run(":8081")
}
先要使用 LoadHTMLGlob() 或者 LoadHTMLFiles()方法来加载模板文件。
创建一个目录:templates,然后在该目录下创建一个模板文件:templates/index.tmpl
<html>
<h1>
{{ .title }}
</h1>
</html>
结果: 不同文件夹下模板名字可以相同,此时需要 LoadHTMLGlob() 加载两层模板路径。
想要得到xxx效果,背后需要用到xxx知识,而xxx知识,是需要自己预先了解的。 go文件代码:
func main(){
router := gin.Default()
router.LoadHTMLGlob("templates/**/*")
router.GET("/posts",func(c *gin.Context){
c.HTML(http.StatusOK,"posts/index.tmpl",gin.H{
"title":"为CSDN代码写注释点赞",
})
})
router.GET("/users",func(c *gin.Context){
c.HTML(http.StatusOK,"users/index.tmpl",gin.H{
"title":"为CSDN代码有力量点赞",
})
})
router.Run(":8081")
}
templates/posts/index.tmpl 代码:
{{define "posts/index.tmpl"}}
<html>
<h1>
{{ .title }}
</h1>
</html>
{{end}}
templates/users/index.tmpl 代码:
{{define "users/index.tmpl"}}
<html>
<h1>
{{ .title }}
</h1>
</html>
{{end}}
结果:
3.3 文件响应
了解文件响应是什么 会使用文件响应的相关操作
可以通过阅读源码的注释了解这个函数function实现了什么功能 / 是什么东西。
代码:
func main(){
router := gin.Default()
router.StaticFS("/showDir",http.Dir("."))
router.Static("/static","D:\\GinWeb\\templates")
router.StaticFile("/imgs/a.png","D:\\GinWeb\\imgs\\a.png")
router.Run(":8081")
}
访问当前项目的目录内容截图: StaticFS的工作方式就像Static() 一样,但是可以使用自定义的http.FileSystem 来代替。
浏览器上获取服务器上的文件: Static从给定的文件系统根目录提供文件 如果你在浏览器里访问http://localhost:8081/static/file.html,你会得到404的错误,这是因为Gin做了安全措施,防止第三方恶意罗列获取你服务器上的所有文件。 在本地的postman中通过测试: 在本地的cmd中通过测试:
StaticFile注册了一个路由,以便访问本地文件系统的一个文件。
参考链接: https://www.flysnow.org/2020/07/21/golang-gin-static-files.html https://learnku.com/docs/gin-gonic/1.5/examples-serving-static-files/6199
3.4 重定向
比如在一个网站上,你注册成功之后会跳转到(重定向到)网站的首页。 代码:
func main(){
router := gin.Default()
router.GET("/redirect",func(c *gin.Context){
c.Redirect(http.StatusMovedPermanently,"https://www.csdn.net")
})
router.Run(":8081")
}
在浏览器上输入:http://127.0.0.1:8081/redirect
会跳转到:
3.5 同步异步
goroutine 机制可以方便地实现异步处理。 当在中间件或处理程序中启动新的Goroutines时,你不应该在原始上下文使用它,你必须使用只读的副本。 同步异步的最简单的理解:
想象一下,在开发中,服务器发送邮件,在这段时间中服务器呈现一个阻塞的状态,也就是等到邮件发送完毕后,服务器才会响应浏览器,用户才能继续浏览网页。
异步就是当发送邮件、或者文件上传,图像处理等等一些比较耗时的操作,我们可将耗时的任务放到后台异步执行,在这段时间内,服务器会正常响应浏览器,用户可以继续浏览网页,这样用户不需要等待很久,提高用户体验。 比如百度网盘上传文件的同时,你可以把这项上传文件的任务晾着放后台不管,后台任务会自行完成所需的文件上传,在这个过程中,你可以在百度网盘里继续做其他事情,比如看海贼王。
代码:
func main(){
router := gin.Default()
router.GET("long_async",func(c *gin.Context){
cp := c.Copy()
go func(){
time.Sleep(5*time.Second)
log.Println("Done in path" + cp.Request.URL.Path)
}()
})
router.GET("long_sync",func(c *gin.Context){
time.Sleep(5 * time.Second)
log.Println("Done in path" + c.Request.URL.Path)
})
router.Run(":8081")
}
4. 中间件
golang的net/http设计的一大特点就是特别容易构建中间件。gin也提供了类似的中间件。需要注意的是中间件只对注册过的路由函数起作用。对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。中间件分为全局中间件,单个路由中间件和群组中间件。
Context 是 Gin 的核心, 它的构造如下:
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain
index int8
engine *Engine
Keys map[string]interface{}
Errors errorMsgs
Accepted []string
}
handlers 我们通过源码可以知道它的类型就是 []HandlerFunc,而它的签名正是:
4.1 全局中间件
我们怎么写 HandlerFunc 就可以怎么写一个中间件。 代码:
func MiddleWare() gin.HandlerFunc{
return func(c *gin.Context){
t := time.Now()
fmt.Println("before middleware......")
c.Set("request","client_request")
c.Next()
status := c.Writer.Status()
fmt.Println("after middlewares,",status)
t2 := time.Since(t)
fmt.Println("time:",t2)
}
}
func main(){
router := gin.Default()
router.Use(MiddleWare())
{
router.GET("/middleware",func(c *gin.Context){
request := c.MustGet("request").(string)
req,_ := c.Get("request")
fmt.Println("request:",request)
c.JSON(http.StatusOK,gin.H{
"middle_request":request,
"request":req,
})
})
router.GET("/before",func(c *gin.Context){
request := c.MustGet("request").(string)
c.JSON(http.StatusOK,gin.H{
"middle_request":request,
})
})
}
router.Run(":8081")
}
结果:
先定义一个中间件函数, 该函数很简单,只会给c上下文添加一个属性并赋值。 后面的路由处理器,可以根据被中间件装饰后提取其值。 需要注意,虽然名为全局中间件,只要注册中间件的过程之前设置的路由,将不会受注册的中间件所影响。 只有注册了中间件以下代码的路由函数规则,才会被中间件装饰。
使用router装饰中间件,然后在/middlerware即可读取request的值,注意在router.Use(MiddleWare())代码以上的路由函数,将不会有被中间件装饰的效果。
使用花括号包含被装饰的路由函数只是一个代码规范,即使没有被包含在内的路由函数,只要使用router进行路由,都等于被装饰了。 想要区分权限范围,可以使用组返回的对象注册中间件。
4.2 Next()方法
怎么解决一个请求和一个响应经中间件呢? 就是 c.Next(),所有中间件都有 Request 和 Response 的分水岭, 就是 c.Next()。
接下来就要剖析gin源码了。 摘抄自 gin简单剖析 api服务创建
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run()
}
3步创建了一个api服务: 1)gin.Default()获得一个Engine实例 2)engine.GET()添加一个Get请求的路由的逻辑 3)engine.Run()启动服务
gin.Default 剖析
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
UnescapePathValues: true,
trees: make(methodTrees, 0, 9),
delims: render.Delims{"{{", "}}"},
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
func Default() *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
gin.Default 通过 New 创建了 Engine 实例, 并 Use 了 Logger Recovery 两个 HandlerFunc 中间件。 注释也介绍了 默认返回一个引擎实例,其中包含日志记录器和崩溃恢复中间件。
func (c *Context) Next() {
c.index++
s := int8(len(c.handlers))
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
代码案例:
func MiddleWare() gin.HandlerFunc{
return func(c *gin.Context){
t := time.Now()
fmt.Println("before middleware......")
c.Set("request","client_request")
c.Next()
status := c.Writer.Status()
fmt.Println("after middlewares,",status)
t2 := time.Since(t)
fmt.Println("time:",t2)
}
}
func main(){
router := gin.Default()
router.Use(MiddleWare())
{
router.GET("/middleware",func(c *gin.Context){
request := c.MustGet("request").(string)
req,_ := c.Get("request")
fmt.Println("request:",request)
c.JSON(http.StatusOK,gin.H{
"middle_request":request,
"request":req,
})
})
router.GET("/before",func(c *gin.Context){
request := c.MustGet("request").(string)
c.JSON(http.StatusOK,gin.H{
"middle_request":request,
})
})
}
router.Run(":8081")
}
gin.Default()获得一个Engine实例, Engine实例中engine.Use(Logger(), Recovery()) 已经使用了Logger()函数, 服务端使用了Use方法导入了middleware , 当请求/middleware 来的时候,会执行MiddleWare() , 并且我们知道在GET注册的时候,同时注册了匿名函数,在gin.Context 对象中会有Next()方法:
Next()方法 摘抄自 从Next()方法我们可以看到它会遍历执行全部handlers(中间件也是handler), 一个请求过来, Gin 会主动调用 c.Next() 一次。因为 handlers 是 slice ,所以后来者中间件会追加到尾部。这样就形成了形如 m1(m2(f())) 的调用链。 正如下面数字① ② 标注的一样, 我们会依次执行如下的调用:
m1① -> m2② -> f -> m2② -> m1①
所以中间件中调不调用Next()方法并不会影响后续中间件的执行。
既然中间件中没有Next()不影响后续中间件的执行,那么在当前中间件中调用c.Next()的作用又是什么呢? 结论是: 在当前中间件中调用c.Next() 方法时会中断当前中间件中后续的逻辑, 转而执行后续的中间件和handlers, 等到它们全部执行完毕之后再回来执行当前中间件/本函数的后续代码。
代码案例执行截图: 在本案例中,代码执行到了c.Next() 的时候,下一步执行注册的匿名函数,然后再回到本函数(包含c.Next() 的函数)继续执行, 所以本案例的Println 的输出顺序是:
fmt.Println("request:",request)
fmt.Println("after middlewares,",status)
fmt.Println("time:",t2)
如果将c.Next() 放到fmt.Println("after middlewares,",status) 后面,那么输出顺序就是:
fmt.Println("after middlewares,",status)
fmt.Println("request:",request)
fmt.Println("time:",t2)
所以一切都取决于c.Next() 执行的位置。
4.3 单个路由中间件
gin提供了对指定的路由函数进行注册。 部分代码:将部分代码放到router.Use(MiddleWare()) 之前,同样也能看到/before 被装饰了中间件,
router.GET("/before",MiddleWare(),func(c *gin.Context){
request := c.MustGet("request").(string)
c.JSON(http.StatusOK,gin.H{
"middle_request":request,
})
})
通过浏览器访问如图所示: 后台输出:一切由c.Next() 位置决定的 + 还有请求了哪些接口。
完整代码:
func MiddleWare() gin.HandlerFunc{
return func(c *gin.Context){
t := time.Now()
fmt.Println("before middleware......")
c.Set("request","client_request")
c.Next()
status := c.Writer.Status()
fmt.Println("after middlewares,",status)
t2 := time.Since(t)
fmt.Println("time:",t2)
}
}
func main(){
router := gin.Default()
router.GET("/before",MiddleWare(),func(c *gin.Context){
request := c.MustGet("request").(string)
c.JSON(http.StatusOK,gin.H{
"middle_request":request,
})
})
router.Use(MiddleWare())
{
router.GET("/middleware",func(c *gin.Context){
request := c.MustGet("request").(string)
req,_ := c.Get("request")
fmt.Println("request:",request)
c.JSON(http.StatusOK,gin.H{
"middle_request":request,
"request":req,
})
})
}
router.Run(":8081")
}
4.4 中间件实践
中间件最大的作用,莫过于用于一些记录log,错误handler,还有就是对部分接口的鉴权。下面就实现一个简易的鉴权中间件。 简单认证BasicAuth 先定义私有数据;
然后使用 gin.BasicAuth 中间件,设置授权用户;
最后定义路由; 完整程序代码:
var secrets = gin.H{
"keagan": gin.H{"email":"keagan@163.com","phone":"18888888888"},
}
func main(){
router := gin.Default()
authorized := router.Group("/admin",gin.BasicAuth(gin.Accounts{
"keagan":"admin123",
}))
authorized.GET("/secrets",func(c *gin.Context){
user := c.MustGet(gin.AuthUserKey).(string)
if secret,ok := secrets[user];ok{
c.JSON(http.StatusOK,gin.H{
"user":user,
"secret":secret,
})
} else{
c.JSON(http.StatusOK,gin.H{
"user":user,
"secret":"NO SECRET",
})
}
})
router.Run(":8081")
}
运行程序,在浏览器输入网址 http://127.0.0.1:8081/admin/secrets,
然后会弹出一个登录框,需要你输入正确的用户名和密码: 登录成功之后:
…我也是有底线的…
|