Gin

SRIGT發表於2024-03-14

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

(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-

相關文章