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-

Gin的更多相关文章

  1. 浅谈postgresql的GIN索引(通用倒排索引)

    1.倒排索引原理 倒排索引来源于搜索引擎的技术,可以说是搜索引擎的基石.正是有了倒排索引技术,搜索引擎才能有效率的进行数据库查找.删除等操作.在详细说明倒排索引之前,我们说一下与之相关的正排索引并与之 ...

  2. 基于gin框架和jwt-go中间件实现小程序用户登陆和token验证

    本文核心内容是利用jwt-go中间件来开发golang webapi用户登陆模块的token下发和验证,小程序登陆功能只是一个切入点,这套逻辑同样适用于其他客户端的登陆处理. 小程序登陆逻辑 小程序的 ...

  3. gin框架使用注意事项

    gin框架使用注意事项 本文就说下这段时间我在使用gin框架过程中遇到的问题和要注意的事情. 错误处理请求返回要使用c.Abort,不要只是return 当在controller中进行错误处理的时候, ...

  4. gin+gorm

    在[环境变量]中添加如下[用户变量]/[系统变量]:GO111MODULE,值为on go mod init目录 在项目中新建文件main.go,并添加测试代码 package main import ...

  5. 使用go, gin, gorm编写一个简单的curd的api接口

    go 是一门非常灵活的语言,既具有静态语言的高性能,又有动态语言的开发速度快的优点,语法也比较简单,下面是通过简单的代码实现了一个简单的增删改查 api 接口 hello world 常规版 新建 d ...

  6. go语言框架gin之集成swagger

    1.安装swag 在goLand中直接使用go get -u github.com/swaggo/swag/cmd/swag命令安装会报错 翻了很多博客,都没找到太合适的办法,根据博客中所写的操作还是 ...

  7. Gin框架源码解析

    Gin框架源码解析 Gin框架是golang的一个常用的web框架,最近一个项目中需要使用到它,所以对这个框架进行了学习.gin包非常短小精悍,不过主要包含的路由,中间件,日志都有了.我们可以追着代码 ...

  8. Gin 路由解析树详解

    说明: 无意间看到gin 中有trees的属性,好奇想一探究竟,到底gin是怎样生成路由解析树的? 这是一个测试截图,图中大概可以了解到gin是怎样做路由解析的.配合源码的阅读,解析树大致如下: 通过 ...

  9. Golang 微框架 Gin 简介

    框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写程序了.成长总不会一蹴而就,从写出程序获取成就感,再到精通框架,快速构造应用,当这些方面都得心应 ...

  10. gin的url查询参数解析

    gin作为go语言最知名的网络库,在这里我简要介绍一下url的查询参数解析.主要是这里面存在一些需要注意的地方.这里,直接给出代码,和运行结果,在必要的地方进行分析. 代码1: type Struct ...

随机推荐

  1. Jetpack的ViewModel与LiveData总结

    本文基于SDK 29 一.ViewModel与LiveData的作用: 1.viewModel: 数据共享,屏幕旋转不丢失数据,并且在Activity与Fragment之间共享数据. 2.LiveDa ...

  2. [java] Tomcat 启动失败 Error: error while reading constant pool for .class: unexpected tag at #

    表现 公司服务器今天启动tomcat失败, 看catalina.out文件里面报错 java.lang.ClassFormatError: Unknown constant tag 101 in cl ...

  3. ElasticSearch基础介绍(1)

    ## 1. Elasticsearch基本介绍 官网:https://www.elastic.co/cn Elasticsearch(简称ES)是一个基于Apache Lucene(TM)的开源搜索引 ...

  4. GaussDB(DWS)运维利刃:TopSQL工具解析

    本文分享自华为云社区<GaussDB(DWS)运维利刃:TopSQL工具解析>,作者:胡辣汤. 在生产环境中,难免会面临查询语句出现异常中断.阻塞时间长等突发问题,如果没能及时记录信息,事 ...

  5. vscode 切换页签快捷键 自定义 Ctrl+H Ctrl+L 左右切换

    今天需要整理写资料,需要在多个页签之间切换,发现自定义了快捷. 好久不用这个快捷键,都快忘了. vscode 切换页签快捷键 自定义 Ctrl+H Ctrl+L 左右切换

  6. 【stars-one】JetBrains产品试用重置工具

    原文[stars-one]JetBrains产品试用重置工具 | Stars-One的杂货小窝 一款可重置JetBrains全家桶产品的试用时间的小工具,与其全网去找激活码,还不如每个月自己手动重置试 ...

  7. C++一些新的特性的理解

    一.智能指针 为什么需要智能指针? 智能指针主要解决一下问题: 内存泄漏:内存手动释放,使用智能指针可以自动释放 共享所有权的指针的传播和释放,比如多线程使用同一个对象时析构的问题. C++里面的四个 ...

  8. Android热点SoftAP使用方式

    一.背景 最近项目中Android设备需要获取SoftAP信息(wifi账号.密码.IP等),然后传递到投屏器中,那么如何获取到SoftAP信息呢?我们知道可以通过WifiManager类里的方法可以 ...

  9. 什么是3D可视化,为什么要使用3D可视化

    虽然许多设计师听说过为什么设计的可视化在他们的审批过程中是有益的,但并不是每个人都知道3D可视化到底是什么. 3D可视化与3D图形.3D渲染.计算机生成图像和其他术语同义使用.3D可视化是指使用计算机 ...

  10. 浅析三维模型3DTile格式轻量化处理常见问题与处理措施

    浅析三维模型3DTile格式轻量化处理常见问题与处理措施 三维模型3DTile格式的轻量化处理是大规模三维地理空间数据可视化的关键环节,但在实际操作过程中,往往会遇到一些问题.下面我们来看一下这些常见 ...