Gin

0x01 准备

(1)概述

  • 定义:一个 golang 的微框架
  • 特点:封装优雅,API 友好,源码注释明确,快速灵活,容错方便
  • 优势:
    1. 对于 golang 而言,web 框架的依赖要远比 Python,Java 之类的要小
    2. 自身的 net/http 足够简单,性能也非常不错
    3. 借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范

(2)安装

Go 语言基础以及 IDE 配置可以参考《Go | 博客园-SRIGT》

  1. 使用命令 go get -u github.com/gin-gonic/gin 安装 Gin 框架

  2. 在项目根目录新建 main.go,在其中引入 Gin

    package main
    
    import "github.com/gin-gonic/gin"
    
    func main() {}
    

(3)第一个页面

  • 修改 main.go

    package main
    
    // 引入 Gin 框架和 http 包
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    // 主函数
    func main() {
        // 创建路由
    	route := gin.Default()
        
        // 绑定路由规则,访问 / 时,执行第二参数为的函数
        // gin.Context 中封装了 request 和 response
    	route.GET("/", func(context *gin.Context) {
    		context.String(http.StatusOK, "Hello, Gin")
    	})
        
        // 监听端口,默认 8080,可以自定义,如 8000
    	route.Run(":8000")
    }
    
  • 编译运行

  • 访问 http://localhost:8000/ 查看页面

0x02 路由

(1)概述

  • Gin 路由库基于 httprouter 构建
  • Gin 支持 Restful 风格的 API
    • URL 描述资源,HTTP 描述操作

(2)获取参数

a. API

  • Gin 可以通过 gin.ContextParams 方法获取参数

  • 举例

    1. 修改 main.go

      package main
      
      import (
      	"fmt"
      	"github.com/gin-gonic/gin"
      	"net/http"
      	"strings"
      )
      
      func main() {
      	route := gin.Default()
      	route.GET("/:name/*action", func(context *gin.Context) {
               // 获取路由规则中 name 的值
      		name := context.Param("name")
              
               // 获取路由规则中 action 的值,并去除字符串两端的 / 号
      		action := strings.Trim(context.Param("action"), "/")
              
      		context.String(http.StatusOK, fmt.Sprintf("%s is %s", name, action))
      	})
      	route.Run(":8000")
      }
      

      :name 捕获一个路由参数,而 *action 则基于通配方法捕获 URL 中 /name/ 之后的所有内容

    2. 访问 http://localhost:8000/SRIGT/studying 查看页面

b. URL

  • 可以通过 DefaultQuery 方法或 Query 方法获取数据

    • 区别在于当参数不存在时:DefaultQuery 方法返回默认值Query 方法返回空串
  • 举例

    1. 修改 main.go

      package main
      
      import (
      	"fmt"
      	"github.com/gin-gonic/gin"
      	"net/http"
      )
      
      func main() {
      	route := gin.Default()
      	route.GET("/", func(context *gin.Context) {
      		// 从 URL 中获取 name 的值,如果 name 不存在,则默认值为 default
      		name := context.DefaultQuery("name", "default")
      
      		// 从 URL 中获取 age 的值
      		age := context.Query("age")
      		
      		context.String(http.StatusOK, fmt.Sprintf("%s is %s years old", name, age))
      	})
      	route.Run(":8000")
      }
      
    2. 访问 http://localhost:8000/?name=SRIGT&age=18http://localhost:8000/?age= 查看页面

c. 表单

  • 表单传输为 POST 请求,HTTP 常见的传输格式为四种

    1. application/json
    2. application/x-www-form-urlencoded
    3. application/xml
    4. multipart/form-data
  • 表单参数可以通过 PostForm 方法获取,该方法默认解析 x-www-form-urlencodedform-data 格式的参数

  • 举例

    1. 在项目根目录新建 index.html

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Document</title>
      </head>
      <body>
      <form method="post" action="http://localhost:8000/" enctype="application/x-www-form-urlencoded">
          <label>Username: <input type="text" name="username" placeholder="Username" /></label>
          <label>Password: <input type="password" name="password" placeholder="Password" /></label>
          <input type="submit" value="Submit" />
      </form>
      </body>
      </html>
      
    2. 修改 main.go

      package main
      
      import (
      	"fmt"
      	"github.com/gin-gonic/gin"
      	"net/http"
      )
      
      func main() {
      	route := gin.Default()
      	route.POST("/", func(context *gin.Context) {
      		types := context.DefaultPostForm("type", "post")
      		username := context.PostForm("username")
      		password := context.PostForm("password")
      		context.String(http.StatusOK, fmt.Sprintf("username: %s\npassword: %s\ntype: %s", username, password, types))
      	})
      	route.Run(":8000")
      }
      
    3. 使用浏览器打开 index.html,填写表单并点击按钮提交

(3)文件上传

a. 单个

  • multipart/form-data 格式用于文件上传

  • 文件上传与原生的 net/http 方法类似,不同在于 Gin 把原生的 request 封装到 context.Request

  • 举例

    1. 修改 index.html

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Document</title>
      </head>
      <body>
      <form method="post" action="http://localhost:8000/" enctype="multipart/form-data">
          <label>Upload: <input type="file" name="file" /></label>
          <input type="submit" value="Submit" />
      </form>
      </body>
      </html>
      
    2. 修改 main.go

      package main
      
      import (
      	"github.com/gin-gonic/gin"
      	"net/http"
      )
      
      func main() {
      	route := gin.Default()
      
      	// 限制文件大小为 8MB
      	//route.MaxMultipartMemory = 8 << 20
      	
      	route.POST("/", func(context *gin.Context) {
      		file, err := context.FormFile("file")
      		if err != nil {
      			context.String(http.StatusInternalServerError, "Error creating file")
      		}
      		context.SaveUploadedFile(file, file.Filename)
      		context.String(http.StatusOK, file.Filename)
      	})
      	route.Run(":8000")
      }
      
    3. 使用浏览器打开 index.html,选择文件并点击按钮提交

    4. 修改 main.go,限定上传文件的类型

      package main
      
      import (
      	"fmt"
      	"github.com/gin-gonic/gin"
      	"log"
      	"net/http"
      )
      
      func main() {
      	route := gin.Default()
      	route.POST("/", func(context *gin.Context) {
      		_, headers, err := context.Request.FormFile("file")
      		if err != nil {
      			log.Printf("Error when creating file: %v", err)
      		}
      		// 限制文件大小在 2MB 以内
      		if headers.Size > 1024*1024*2 {
      			fmt.Printf("Too big")
      			return
      		}
      
      		// 限制文件类型为 PNG 图片文件
      		if headers.Header.Get("Content-Type") != "image/png" {
      			fmt.Printf("Only PNG is supported")
      			return
      		}
      
      		context.SaveUploadedFile(headers, "./upload/"+headers.Filename)
      		context.String(http.StatusOK, headers.Filename)
      	})
      	route.Run(":8000")
      }
      
    5. 刷新页面,选择文件并点击按钮提交

b. 多个

  1. 修改 index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
    <form method="post" action="http://localhost:8000/" enctype="multipart/form-data">
        <label>Upload: <input type="file" name="files" multiple /></label>
        <input type="submit" value="Submit" />
    </form>
    </body>
    </html>
    
  2. 修改 main.go

    package main
    
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func main() {
    	route := gin.Default()
    	route.POST("/", func(context *gin.Context) {
    		form, err := context.MultipartForm()
    		if err != nil {
    			context.String(http.StatusBadRequest, fmt.Sprintf("get err %s", err.Error()))
    		}
    		files := form.File["files"]
    		for _, file := range files {
    			if err := context.SaveUploadedFile(file, "./upload/"+file.Filename); err != nil {
    				context.String(http.StatusBadRequest, fmt.Sprintf("upload err %s", err.Error()))
    				return
    			}
    		}
    		context.String(http.StatusOK, fmt.Sprintf("%d files uploaded", len(files)))
    	})
    	route.Run(":8000")
    }
    
  3. 使用浏览器打开 index.html,选择多个文件并点击按钮提交

(4)路由组

  • 路由组(routes group)用于管理一些相同的 URL

  • 举例

    1. 修改 main.go

      package main
      
      import (
      	"fmt"
      	"github.com/gin-gonic/gin"
      	"net/http"
      )
      
      func main() {
      	route := gin.Default()
      
      	// 路由组一,处理 GET 请求
      	v1 := route.Group("/v1")
      	{
      		v1.GET("/login", login)
      		v1.GET("/submit", submit)
      	}
      
      	// 路由组二,处理 POST 请求
      	v2 := route.Group("/v2")
      	{
      		v2.POST("/login", login)
      		v2.POST("/submit", submit)
      	}
      
      	route.Run(":8000")
      }
      
      func login(context *gin.Context) {
      	name := context.DefaultQuery("name", "defaultLogin")
      	context.String(http.StatusOK, fmt.Sprintf("hello, %s\n", name))
      }
      
      func submit(context *gin.Context) {
      	name := context.DefaultQuery("name", "defaultSubmit")
      	context.String(http.StatusOK, fmt.Sprintf("hello, %s\n", name))
      }
      
    2. 使用 Postman 对以下链接测试 GET 或 POST 请求

      1. http://localhost:8000/v1/login
      2. http://localhost:8000/v1/submit
      3. http://localhost:8000/v2/login
      4. http://localhost:8000/v2/submit

(5)路由原理

  • httprouter 会将所有路由规则构造一棵前缀树

  • 举例:有路由规则为 root and as at cn com,则前缀树为

    graph TB root-->a & c a-->n1[n] & s & t n1-->d c-->n2[n] & o[o] o[o]-->m

(6)路由拆分与注册

a. 基本注册

  • 适用于路由条目较少的项目中

  • 修改main.go,将路由直接注册到 main.go 中

    package main
    
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func main() {
    	route := gin.Default()
    	route.GET("/", login)
    	if err := route.Run(":8000"); err != nil {
    		fmt.Printf("start service failed, error: %v", err)
    	}
    }
    
    func login(context *gin.Context) {
    	context.JSON(http.StatusOK, "Login")
    }
    

b. 拆分成独立文件

  • 当路由条目更多时,将路由部分拆分成一个独立的文件或包

拆分成独立文件

  1. 在下面根目录新建 routers.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func login(context *gin.Context) {
    	context.JSON(http.StatusOK, "Login")
    }
    
    func setupRouter() *gin.Engine {
    	route := gin.Default()
    	route.GET("/", login)
    	return route
    }
    
  2. 修改 main.go

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	route := setupRouter()
    	if err := route.Run(":8000"); err != nil {
    		fmt.Printf("start service failed, error: %v", err)
    	}
    }
    

拆分成包

  1. 新建 router 目录,将 routes.go 移入并修改

    package router
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func login(context *gin.Context) {
    	context.JSON(http.StatusOK, "Login")
    }
    
    func SetupRouter() *gin.Engine {
    	route := gin.Default()
    	route.GET("/", login)
    	return route
    }
    

    setupRouter 从小驼峰命名法改为大驼峰命名法,即 SetupRouter

  2. 修改 main.go

    package main
    
    import (
    	"GinProject/router"
    	"fmt"
    )
    
    func main() {
    	route := router.SetupRouter()
    	if err := route.Run(":8000"); err != nil {
    		fmt.Printf("start service failed, error: %v", err)
    	}
    }
    

c. 拆分成多个文件

  • 当路由条目更多时,将路由文件拆分成多个文件,此时需要使用包
  1. ~/routers 目录下新建 login.go、logout.go

    • login.go

      package router
      
      import (
      	"github.com/gin-gonic/gin"
      	"net/http"
      )
      
      func login(context *gin.Context) {
      	context.JSON(http.StatusOK, "Login")
      }
      
      func LoadLogin(engin *gin.Engine) {
      	engin.GET("/login", login)
      }
      
    • logout.go

      package router
      
      import (
      	"github.com/gin-gonic/gin"
      	"net/http"
      )
      
      func logout(context *gin.Context) {
      	context.JSON(http.StatusOK, "Logout")
      }
      
      func LoadLogout(engin *gin.Engine) {
      	engin.GET("/logout", logout)
      }
      
  2. 修改 main.go

    package main
    
    import (
    	"GinProject/routers"
    	"fmt"
    	"github.com/gin-gonic/gin"
    )
    
    func main() {
    	route := gin.Default()
    	routers.LoadLogin(route)
    	routers.LoadLogout(route)
    	if err := route.Run(":8000"); err != nil {
    		fmt.Printf("start service failed, error: %v", err)
    	}
    }
    

d. 拆分到多个 App

目录结构:

graph TB 根目录-->app & go.mod & main.go & routers app-->login-->li[router.go] app-->logout-->lo[router.go] routers-->routers.go
  1. ~/app/login/router.go

    package login
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func login(context *gin.Context) {
    	context.JSON(http.StatusOK, "Login")
    }
    
    func Routers(engine *gin.Engine) {
    	engine.GET("/login", login)
    }
    
  2. ~/app/logout/router.go

    package logout
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func logout(context *gin.Context) {
    	context.JSON(http.StatusOK, "Logout")
    }
    
    func Routers(engine *gin.Engine) {
    	engine.GET("/logout", logout)
    }
    
  3. ~/routers/routers.go

    package routers
    
    import "github.com/gin-gonic/gin"
    
    type Option func(engine *gin.Engine)
    
    var options = []Option{}
    
    func Include(params ...Option) {
    	options = append(options, params...)
    }
    
    func Init() *gin.Engine {
    	route := gin.New()
    	for _, option := range options {
    		option(route)
    	}
    	return route
    }
    
    • 定义 Include 函数来注册 app 中定义的路由
    • 使用 Init 函数来进行路由的初始化操作
  4. 修改 main.go

    package main
    
    import (
    	"GinProject/login"
    	"GinProject/logout"
    	"GinProject/routers"
    	"fmt"
    )
    
    func main() {
    	routers.Include(login.Routers, logout.Routers)
    	route := routers.Init()
    	if err := route.Run(":8000"); err != nil {
    		fmt.Printf("start service failed, error: %v", err)
    	}
    }
    

0x03 数据解析与绑定

(1)JSON

  1. 修改 main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    // 定义接收数据的结构体
    type Login struct {
    	User     string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
    	Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
    }
    
    func main() {
    	route := gin.Default()
    	route.POST("/login", func(context *gin.Context) {
             // 声明接收的变量
    		var json Login
    
             // 解析 json 数据到结构体
    		if err := context.ShouldBindJSON(&json); err != nil {
    			context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    			return
    		}
    
             // 数据验证
    		if json.User != "root" || json.Password != "admin" {
    			context.JSON(http.StatusNotModified, gin.H{"message": "Username or password is incorrect"})
    			return
    		}
    
    		context.JSON(http.StatusOK, gin.H{"message": "Login successful"})
    	})
    	route.Run(":8000")
    }
    
  2. 使用 Postman 模拟客户端传参(body/raw/json)

(2)表单

  1. 修改 index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
    <form method="post" action="http://localhost:8000/login" enctype="application/x-www-form-urlencoded">
        <label>Username: <input type="text" name="username" placeholder="Username" /></label>
        <label>Password: <input type="password" name="password" placeholder="Password" /></label>
        <input type="submit" value="Submit" />
    </form>
    </body>
    </html>
    
  2. 修改 main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    type Login struct {
    	User     string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
    	Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
    }
    
    func main() {
    	route := gin.Default()
    	route.POST("/login", func(context *gin.Context) {
    		var form Login
    		if err := context.Bind(&form); err != nil {
    			context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    			return
    		}
    		if form.User != "root" || form.Password != "admin" {
    			context.JSON(http.StatusNotModified, gin.H{"message": "Username or password is incorrect"})
    			return
    		}
    		context.JSON(http.StatusOK, gin.H{"message": "Login successful"})
    	})
    	route.Run(":8000")
    }
    
  3. 使用浏览器打开 index.html,填写表单并点击按钮提交

(3)URI

  1. 修改 main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    type Login struct {
    	User     string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
    	Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
    }
    
    func main() {
    	route := gin.Default()
    	route.GET("/login/:user/:password", func(context *gin.Context) {
    		var uri Login
    		if err := context.ShouldBindUri(&uri); err != nil {
    			context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    			return
    		}
    		if uri.User != "root" || uri.Password != "admin" {
    			context.JSON(http.StatusNotModified, gin.H{"message": "Username or password is incorrect"})
    			return
    		}
    		context.JSON(http.StatusOK, gin.H{"message": "Login successful"})
    	})
    	route.Run(":8000")
    }
    
  2. 访问 http://localhost:8000/login/root/admin 查看页面

0x04 渲染

(1)数据格式的响应

  • JSON、结构体、XML、YAML 类似于 Java 中的 propertiesProtoBuf

  • 举例

    1. JSON

      route.GET("/", func(context *gin.Context) {
      	context.JSON(http.StatusOK, gin.H{"message": "JSON"})
      })
      
    2. 结构体

      route.GET("/", func(context *gin.Context) {
      	var msg struct{
      		Message string
      	}
      	msg.Message = "Struct"
      	context.JSON(http.StatusOK, msg)
      })
      
    3. XML

      route.GET("/", func(context *gin.Context) {
      	context.XML(http.StatusOK, gin.H{"message": "XML"})
      })
      
    4. YAML

      route.GET("/", func(context *gin.Context) {
      	context.YAML(http.StatusOK, gin.H{"message": "YAML"})
      })
      
    5. ProtoBuf

      route.GET("/", func(context *gin.Context) {
      	reps := []int64{int64(0), int64(1)}
      	label := "Label"
      	data := &protoexample.Test{
      		Reps:  reps,
      		Label: &label,
      	}
      	context.XML(http.StatusOK, gin.H{"message": data})
      })
      

(2)HTML 模板渲染

  • Gin 支持加载 HTML 模板,之后根据模板参数进行配置,并返回相应的数据
  • 引入静态文件目录:route.Static("/assets", "./assets")
  • LoadHTMLGlob() 方法可以加载模板文件

a. 默认模板

目录结构:

graph TB 根目录-->tem & main.go & go.mod tem-->index.html
  1. index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>{{ .title }}</title>
    </head>
    <body>
    Content: {{ .content }}
    </body>
    </html>
    
  2. main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func main() {
    	route := gin.Default()
    	route.LoadHTMLGlob("tem/*")
    	route.GET("/", func(context *gin.Context) {
    		context.HTML(http.StatusOK, "index.html", gin.H{"title": "Document", "content": "content"})
    	})
    	route.Run(":8000")
    }
    
  3. 访问 http://localhost:8000/ 查看页面

b. 子模板

目录结构:

graph TB 根目录-->tem & main.go & go.mod tem-->page-->index.html
  1. index.html

    {{ define "page/index.html" }}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>{{ .title }}</title>
    </head>
    <body>
    Content: {{ .content }}
    </body>
    </html>
    {{ end }}
    
  2. main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func main() {
    	route := gin.Default()
    	route.LoadHTMLGlob("tem/**/*")
    	route.GET("/", func(context *gin.Context) {
    		context.HTML(http.StatusOK, "page/index.html", gin.H{"title": "Document", "content": "content"})
    	})
    	route.Run(":8000")
    }
    
  3. 访问 http://localhost:8000/ 查看页面

c. 组合模板

目录结构:

graph TB 根目录-->tem & main.go & go.mod tem-->page & public public-->header.html & footer.html page-->index.html
  1. header.html

    {{ define "public/header" }}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>{{ .title }}</title>
    </head>
    <body>
    {{ end }}
    
  2. footer.html

    {{ define "public/footer" }}
    </body>
    </html>
    {{ end }}
    
  3. index.html

    {{ define "page/index.html" }}
    {{ template "public/header" }}
    Content: {{ .content }}
    {{ template "public/footer" }}
    {{ end }}
    
  4. main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func main() {
    	route := gin.Default()
    	route.LoadHTMLGlob("tem/**/*")
    	route.GET("/", func(context *gin.Context) {
    		context.HTML(http.StatusOK, "page/index.html", gin.H{"title": "Document", "content": "content"})
    	})
    	route.Run(":8000")
    }
    
  5. 访问 http://localhost:8000/ 查看页面

(3)重定向

  1. 修改 main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func main() {
    	route := gin.Default()
    	route.GET("/", func(context *gin.Context) {
    		context.Redirect(http.StatusMovedPermanently, "https://www.cnblogs.com/SRIGT")
    	})
    	route.Run(":8000")
    }
    
  2. 访问 http://localhost:8000/ 查看页面

(4)同步与异步

  • goroutine 机制可以实现异步处理
  • 启动新的 goroutine 时,不应该使用原始上下文,必须使用它的副本
  1. 修改 main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"log"
    	"time"
    )
    
    func main() {
    	route := gin.Default()
    
    	// 异步
    	route.GET("/async", func(context *gin.Context) {
    		copyContext := context.Copy()
    		go func() {
    			time.Sleep(3 * time.Second)
    			log.Println("Async: " + copyContext.Request.URL.Path)
    		}()
    	})
    
    	// 同步
    	route.GET("/sync", func(context *gin.Context) {
    		time.Sleep(3 * time.Second)
    		log.Println("Sync: " + context.Request.URL.Path)
    	})
    
    	route.Run(":8000")
    }
    
  2. 访问 http://localhost:8000/ 查看页面

0x05 中间件

(1)全局中间件

  • 所有请求都会经过全局中间件
  1. 修改 main.go

    package main
    
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"net/http"
    	"time"
    )
    
    // 定义中间件
    func Middleware() gin.HandlerFunc {
    	return func(context *gin.Context) {
    		timeStart := time.Now()
    		fmt.Println("Middleware starting")
    		context.Set("request", "middleware")
    		status := context.Writer.Status()
    		fmt.Println("Middleware stopped", status)
    		timeEnd := time.Since(timeStart)
    		fmt.Println("Time elapsed: ", timeEnd)
    	}
    }
    
    func main() {
    	route := gin.Default()
    	route.Use(Middleware())	// 注册中间件
    	{	// 使用大括号是代码规范
    		route.GET("/", func(context *gin.Context) {
    			req, _ := context.Get("request")
    			fmt.Println("request: ", req)
    			context.JSON(http.StatusOK, gin.H{"request": req})
    		})
    	}
    	route.Run(":8000")
    }
    
  2. 运行

(2)Next 方法

  • Next() 是一个控制流的方法,它决定了是否继续执行后续的中间件或路由处理函数
  1. 修改 main.go

    // ...
    
    func Middleware() gin.HandlerFunc {
    	return func(context *gin.Context) {
    		timeStart := time.Now()
    		fmt.Println("Middleware starting")
    		context.Set("request", "middleware")
    
    		context.Next()
    		
    		status := context.Writer.Status()
    		fmt.Println("Middleware stopped", status)
    		timeEnd := time.Since(timeStart)
    		fmt.Println("Time elapsed: ", timeEnd)
    	}
    }
    // ...
    
  2. 运行

(3)局部中间件

  1. 修改 main.go

    // ...
    func main() {
    	route := gin.Default()
    	{
    		route.GET("/", Middleware(), func(context *gin.Context) {
    			req, _ := context.Get("request")
    			fmt.Println("request: ", req)
    			context.JSON(http.StatusOK, gin.H{"request": req})
    		})
    	}
    	route.Run(":8000")
    }
    
  2. 运行

0x06 会话控制

(1)Cookie

a. 概述

  • 简介:Cookie 实际上就是服务器保存在浏览器上的一段信息,浏览器有了 Cookie 之后,每次向服务器发送请求时都会同时将该信息发送给服务器,服务器收到请求后,就可以根据该信息处理请求 Cookie 由服务器创建,并发送给浏览器,最终由浏览器保存
  • 缺点:采用明文、增加带宽消耗、可被禁用、存在上限

b. 使用

  • 测试服务端发送 Cookie 给客户端,客户端请求时携带 Cookie

    1. 修改 main.go

      package main
      
      import (
      	"fmt"
      	"github.com/gin-gonic/gin"
      )
      
      func main() {
      	route := gin.Default()
      	route.GET("/", func(context *gin.Context) {
      		cookie, err := context.Cookie("key_cookie")
      		if err != nil {
      			cookie = "NotSet"
      			context.SetCookie("key_cookie", "value_cookie", 60, "/", "localhost", false, true)
      		}
      		fmt.Println("Cookie: ", cookie)
      	})
      	route.Run(":8000")
      }
      
      • SetCookie(name, value, maxAge, path, domain, secure, httpOnly)
        • name:Cookie 名称,字符串
        • value:Cookie 值,字符串
        • maxAge:Cookie 生存时间(秒),整型
        • path:Cookie 所在目录,字符串
        • domain:域名,字符串
        • secure:是否只能通过 HTTPS 访问,布尔型
        • httpOnly:是否允许通过 Javascript 获取 Cookie 布尔型
    2. 访问 http://localhost:8000/,此时输出 “Cookie: NotSet”

    3. 刷新页面,此时输出 “Cookie: value_cookie”

  • 模拟实现权限验证中间件

    说明:

    1. 路由 login 用于设置 Cookie
    2. 路由 home 用于访问信息
    3. 中间件用于验证 Cookie
    1. 修改 main.go

      package main
      
      import (
      	"github.com/gin-gonic/gin"
      	"net/http"
      )
      
      func main() {
      	route := gin.Default()
      	route.GET("/login", func(context *gin.Context) {
      		context.SetCookie("key", "value", 60, "/", "localhost", false, true)
      		context.String(http.StatusOK, "Login successful")
      	})
      	route.GET("/home", Middleware(), func(context *gin.Context) {
      		context.JSON(http.StatusOK, gin.H{"data": "secret"})
      	})
      	route.Run(":8000")
      }
      
      func Middleware() gin.HandlerFunc {
      	return func(context *gin.Context) {
      		if cookie, err := context.Cookie("key"); err == nil {
      			if cookie == "value" {
      				context.Next()
      				return
      			}
      		}
      		context.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid cookie"})
      		context.Abort()
      		return
      	}
      }
      
    2. 依次访问以下页面

      1. http://localhost:8000/home
      2. http://localhost:8000/login
      3. http://localhost:8000/home
      4. 等待 60 秒后刷新页面

(2)Sessions

  • gorilla/sessions 为自定义 Session 后端提供 Cookie 和文件系统 Session 以及基础结构
  1. 修改 main.go

    package main
    
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"github.com/gorilla/sessions"
    	"net/http"
    )
    
    var store = sessions.NewCookieStore([]byte("secret-key"))
    
    func main() {
    	r := gin.Default()
    
    	// 设置路由
    	r.GET("/set", SetSession)
    	r.GET("/get", GetSession)
    	r.GET("/del", DelSession)
    
    	// 运行服务器
    	r.Run(":8000")
    }
    
    func SetSession(context *gin.Context) {
    	// 获取一个 Session 对象以及名称
    	session, err := store.Get(context.Request, "session-name")
    	if err != nil {
    		context.String(http.StatusInternalServerError, "Error getting session: %s", err)
    		return
    	}
    
    	// 在 Session 中存储键值对
    	session.Values["content"] = "text"
    	session.Values["key1"] = 1 // 注意:session.Values 的键应为字符串类型
    
    	// 保存 Session 修改
    	if err := session.Save(context.Request, context.Writer); err != nil {
    		context.String(http.StatusInternalServerError, "Error saving session: %s", err)
    		return
    	}
    
    	context.String(http.StatusOK, "Session set successfully")
    }
    
    func GetSession(context *gin.Context) {
    	session, err := store.Get(context.Request, "session-name")
    	if err != nil {
    		context.String(http.StatusInternalServerError, "Error getting session: %s", err)
    		return
    	}
    
    	if content, exists := session.Values["content"]; exists {
    		fmt.Println(content)
    		context.String(http.StatusOK, "Session content: %s", content)
    	} else {
    		context.String(http.StatusOK, "No content in session")
    	}
    }
    
    func DelSession(context *gin.Context) {
    	session, err := store.Get(context.Request, "session-name")
    	if err != nil {
    		context.String(http.StatusInternalServerError, "Error getting session: %s", err)
    		return
    	}
    
    	session.Options.MaxAge = -1
    	if err := session.Save(context.Request, context.Writer); err != nil {
    		context.String(http.StatusInternalServerError, "Error deleting session: %s", err)
    		return
    	}
    	context.String(http.StatusOK, "Session delete successfully")
    }
    
  2. 依次访问以下页面

    1. http://localhost:8000/get
    2. http://localhost:8000/set
    3. http://localhost:8000/get
    4. http://localhost:8000/del
    5. http://localhost:8000/get

0x07 参数验证

(1)结构体验证

  • 使用 Gin 框架的数据验证,可以不用解析数据,减少 if...else,会简洁很多
  1. 修改 main.go

    package main
    
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    type Person struct {
    	Name string `form:"name" binding:"required"`
    	Age  int    `form:"age" binding:"required"`
    }
    
    func main() {
    	route := gin.Default()
    	route.GET("/", func(context *gin.Context) {
    		var person Person
    		if err := context.ShouldBind(&person); err != nil {
    			context.String(http.StatusInternalServerError, fmt.Sprint(err))
    			return
    		}
    		context.String(http.StatusOK, fmt.Sprintf("%#v", person))
    	})
    	route.Run(":8000")
    }
    
  2. 访问 http://localhost:8000/?name=SRIGT&age=18 查看页面

(2)自定义验证

  • 对绑定解析到结构体上的参数,自定义验证功能
  • 步骤分为
    1. 自定义校验方法
    2. binding 中使用自定义的校验方法函数注册的名称
    3. 将自定义的校验方法注册到 validator
  1. 修改 main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"github.com/go-playground/validator/v10"
    	"net/http"
    )
    
    type Person struct {
    	Name string `form:"name" validate:"NotNullOrAdmin"`
    	Age  int    `form:"age" validate:"required"`
    }
    
    var validate *validator.Validate
    
    func init() {
    	validate = validator.New()
    	validate.RegisterValidation("NotNullOrAdmin", notNullOrAdmin)
    }
    
    func notNullOrAdmin(fl validator.FieldLevel) bool {
    	value := fl.Field().String()
    	return value != "" && value != "admin"
    }
    
    func main() {
    	route := gin.Default()
    
    	route.GET("/", func(c *gin.Context) {
    		var person Person
    		if err := c.ShouldBind(&person); err == nil {
    			err = validate.Struct(person)
    			if err != nil {
    				c.String(http.StatusBadRequest, "Validation error: %v", err.Error())
    				return
    			}
    			c.String(http.StatusOK, "%v", person)
    		} else {
    			c.String(http.StatusBadRequest, "Binding error: %v", err.Error())
    		}
    	})
    
    	route.Run(":8000")
    }
    
  2. 依次访问以下页面

    1. http://localhost:8000/?age=18
    2. http://localhost:8000/?name=admin&age=18
    3. http://localhost:8000/?name=SRIGT&age=18

(3)多语言翻译验证

举例:返回信息自定义,手机端返回的中文信息,pc 端返回的英文信息,需要做到请求一个接口满足上述三种情况

  1. 修改 main.go

    package main
    
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"github.com/go-playground/locales/en"
    	"github.com/go-playground/locales/zh"
    	ut "github.com/go-playground/universal-translator"
    	"gopkg.in/go-playground/validator.v9"
    	en_translations "gopkg.in/go-playground/validator.v9/translations/en"
    	zh_translations "gopkg.in/go-playground/validator.v9/translations/zh"
    	"net/http"
    )
    
    var (
    	Uni      *ut.UniversalTranslator
    	Validate *validator.Validate
    )
    
    type User struct {
    	Str1 string `form:"str1" validate:"required"`
    	Str2 string `form:"str2" validate:"required,lt=10"`
    	Str3 string `form:"str3" validate:"required,gt=1"`
    }
    
    func main() {
    	en := en.New()
    	zh := zh.New()
    	Uni = ut.New(en, zh)
    	Validate = validator.New()
    
    	route := gin.Default()
    	route.GET("/", home)
    	route.POST("/", home)
    	route.Run(":8000")
    }
    
    func home(context *gin.Context) {
    	locale := context.DefaultQuery("locate", "zh")
    	trans, _ := Uni.GetTranslator(locale)
    	switch locale {
    	case "en":
    		en_translations.RegisterDefaultTranslations(Validate, trans)
    		break
    	case "zh":
    	default:
    		zh_translations.RegisterDefaultTranslations(Validate, trans)
    		break
    	}
    	Validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
    		return ut.Add("required", "{0} must have a value", true)
    	}, func(ut ut.Translator, fe validator.FieldError) string {
    		t, _ := ut.T("required", fe.Field())
    		return t
    	})
    
    	user := User{}
    	context.ShouldBind(&user)
    	fmt.Println(user)
    	err := Validate.Struct(user)
    	if err != nil {
    		errs := err.(validator.ValidationErrors)
    		sliceErrs := []string{}
    		for _, e := range errs {
    			sliceErrs = append(sliceErrs, e.Translate(trans))
    		}
    		context.String(http.StatusOK, fmt.Sprintf("%#v", sliceErrs))
    	}
    	context.String(http.StatusOK, fmt.Sprintf("%#v", user))
    }
    
  2. 依次访问以下页面

    1. http://localhost:8000/?str1=abc&str2=def&str3=ghi&locale=zh
    2. http://localhost:8000/?str1=abc&str2=def&str3=ghi&locale=en

0x08 其他

(1)日志文件

  1. 修改 main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"io"
    	"net/http"
    	"os"
    )
    
    func main() {
    	gin.DisableConsoleColor()
    
    	// 将日志写入 gin.log
    	file, _ := os.Create("gin.log")
    	gin.DefaultWriter = io.MultiWriter(file) // 只写入日志
    	//gin.DefaultWriter = io.MultiWriter(file, os.Stdout)	// 写入日志的同时在控制台输出
    
    	route := gin.Default()
    	route.GET("/", func(context *gin.Context) {
    		context.String(http.StatusOK, "text")
    	})
    	route.Run(":8000")
    }
    
  2. 运行后,查看文件 gin.log

(2)Air 热更新

a. 概述

  • Air 能够实时监听项目的代码文件,在代码发生变更之后自动重新编译并执行,大大提高 Gin 框架项目的开发效率
  • 特性:
    • 彩色的日志输出
    • 自定义构建或必要的命令
    • 支持外部子目录
    • 在 Air 启动之后,允许监听新创建的路径
    • 更棒的构建过程

b. 安装与使用

Air 仓库:https://github.com/cosmtrek/air

  1. 使用命令 go install github.com/cosmtrek/air@latest 安装最新版的 Air
  2. GOPATH/pkg/mod/github.com/cosmtrek/air 中,将 air.exe 文件复制到 GOROOT/bin
    • 如果没有 air.exe 文件,可以使用命令 go build . 生成 air.exe
  3. 使用命令 air -v 确认 Air 是否安装成功
  4. 在项目根目录下,使用命令 air init 生成 Air 配置文件 .air.toml
  5. 使用命令 air 编译项目并实现热更新

(3)验证码

  • 验证码一般用于防止某些接口被恶意调用

  • 实现步骤

    1. 提供一个路由,在 Session 中写入键值对,并将值写在图片上,发送到客户端
    2. 客户端将填写结果返送给服务端,服务端从 Session 中取值并验证
  • 举例

    1. 修改 index.html

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Document</title>
      </head>
      <body>
      <img src="/" onclick="this.src='/?v=' + Math.random()" />
      </body>
      </html>
      
    2. 修改 main.go

      package main
      
      import (
      	"bytes"
      	"github.com/dchest/captcha"
      	"github.com/gin-contrib/sessions"
      	"github.com/gin-contrib/sessions/cookie"
      	"github.com/gin-gonic/gin"
      	"net/http"
      	"time"
      )
      
      // 这个函数用于创建一个会话中间件,它接受一个keyPairs字符串作为参数,用于加密会话。它使用SessionConfig函数配置的会话存储
      func Session(keyPairs string) gin.HandlerFunc {
      	store := SessionConfig()
      	return sessions.Sessions(keyPairs, store)
      }
      
      // 配置会话存储的函数,设置了会话的最大存活时间和加密密钥。这里使用的是 Cookie 存储方式
      func SessionConfig() sessions.Store {
      	sessionMaxAge := 3600
      	sessionSecret := "secret-key"
      	var store sessions.Store
      	store = cookie.NewStore([]byte(sessionSecret))
      	store.Options(sessions.Options{
      		MaxAge: sessionMaxAge,
      		Path:   "/",
      	})
      	return store
      }
      
      // 生成验证码的函数。它可以接受可选的参数来定制验证码的长度、宽度和高度。生成的验证码 ID 存储在会话中,以便后续验证
      func Captcha(context *gin.Context, length ...int) {
      	dl := captcha.DefaultLen
      	width, height := 107, 36
      	if len(length) == 1 {
      		dl = length[0]
      	}
      	if len(length) == 2 {
      		width = length[1]
      	}
      	if len(length) == 3 {
      		height = length[2]
      	}
      	captchaId := captcha.NewLen(dl)
      	session := sessions.Default(context)
      	session.Set("captcha", captchaId)
      	_ = session.Save()
      	_ = Serve(context.Writer, context.Request, captchaId, ".png", "zh", false, width, height)
      }
      
      // 验证用户输入的验证码是否正确。它从会话中获取之前存储的验证码ID,然后使用 captcha.VerifyString 函数进行验证
      func CaptchaVerify(context *gin.Context, code string) bool {
      	session := sessions.Default(context)
      	if captchaId := session.Get("captcha"); captchaId != nil {
      		session.Delete("captcha")
      		_ = session.Save()
      		if captcha.VerifyString(captchaId.(string), code) {
      			return true
      		} else {
      			return false
      		}
      	} else {
      		return false
      	}
      }
      
      // 根据验证码ID生成并返回验证码图片或音频。它设置了响应的HTTP头以防止缓存,并根据请求的文件类型(图片或音频)生成相应的内容
      func Serve(writer http.ResponseWriter, request *http.Request, id, ext, lang string, download bool, width, height int) error {
      	writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
      	writer.Header().Set("Pragma", "no-cache")
      	writer.Header().Set("Expires", "0")
      
      	var content bytes.Buffer
      	switch ext {
      	case ".png":
      		writer.Header().Set("Content-Type", "image/png")
      		_ = captcha.WriteImage(&content, id, width, height)
      	case ".wav":
      		writer.Header().Set("Content-Type", "audio/x-wav")
      		_ = captcha.WriteAudio(&content, id, lang)
      	default:
      		return captcha.ErrNotFound
      	}
      
      	if download {
      		writer.Header().Set("Content-Type", "application/octet-stream")
      	}
      	http.ServeContent(writer, request, id+ext, time.Time{}, bytes.NewReader(content.Bytes()))
      	return nil
      }
      
      func main() {
      	route := gin.Default()
      	route.LoadHTMLGlob("./*.html")
      	route.Use(Session("secret-key"))
      	route.GET("/captcha", func(context *gin.Context) {
      		Captcha(context, 4)
      	})
      	route.GET("/", func(context *gin.Context) {
      		context.HTML(http.StatusOK, "index.html", nil)
      	})
      	route.GET("/captcha/verify/:value", func(context *gin.Context) {
      		value := context.Param("value")
      		if CaptchaVerify(context, value) {
      			context.JSON(http.StatusOK, gin.H{"status": 0, "message": "success"})
      		} else {
      			context.JSON(http.StatusOK, gin.H{"status": 1, "message": "failed"})
      		}
      	})
      	route.Run(":8000")
      }
      
    3. 依次访问以下页面

      1. 获取验证码图片:http://localhost:8000/captcha
      2. 提交结果并验证:http://localhost:8000/captcha/verify/xxxx

(4)生成解析 token

  • 有很多将身份验证内置到 API 中的方法,如 JWT(JSON Web Token)

  • 举例:获取 JWT,检查 JWT

    1. 修改 main.go

      package main
      
      import (
      	"fmt"
      	"github.com/dgrijalva/jwt-go"
      	"github.com/gin-gonic/gin"
      	"net/http"
      	"time"
      )
      
      var jwtKey = []byte("secret-key") // JWT 密钥
      var str string                    // JWT 全局存储
      
      type Claims struct {
      	UserId uint
      	jwt.StandardClaims
      }
      
      func main() {
      	route := gin.Default()
      	route.GET("/set", setFunc)
      	route.GET("/get", getFunc)
      	route.Run(":8000")
      }
      
      // 签发 Token
      func setFunc(context *gin.Context) {
      	expireTime := time.Now().Add(7 * 24 * time.Hour)
      	claims := &Claims{
      		UserId: 1,
      		StandardClaims: jwt.StandardClaims{
      			ExpiresAt: expireTime.Unix(), // 过期时间
      			IssuedAt:  time.Now().Unix(), // 签发时间
      			Issuer:    "127.0.0.1",       // 签发者
      			Subject:   "user token",      // 签名主题
      		},
      	}
      	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
      	tokenString, err := token.SignedString(jwtKey)
      	if err != nil {
      		fmt.Println(err)
      	}
      	str = tokenString
      	context.JSON(http.StatusOK, gin.H{"token": str})
      }
      
      // 验证 Token
      func getFunc(context *gin.Context) {
      	tokenString := context.GetHeader("Authorization")
      	if tokenString == "" {
      		context.JSON(http.StatusUnauthorized, gin.H{"message": "No token"})
      		context.Abort()
      		return
      	}
      
      	token, claims, err := ParseToken(tokenString)
      	if err != nil || token.Valid {
      		context.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid token"})
      		context.Abort()
      		return
      	}
      	fmt.Println("secret data")
      	fmt.Println(claims.UserId)
      }
      
      // 解析 Token
      func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
      	Claims := &Claims{}
      	token, err := jwt.ParseWithClaims(tokenString, Claims, func(token *jwt.Token) (i interface{}, err error) {
      		return jwtKey, nil
      	})
      	return token, Claims, err
      }
      
    2. 依次访问以下页面

      1. http://localhost:8000/get
      2. http://localhost:8000/set
      3. http://localhost:8000/get

-End-

posted @ 2024-03-14 00:35  SRIGT  阅读(18)  评论(0编辑  收藏  举报