0x01 準備
(1)概述
- 定義:一個 golang 的微框架
- 特點:封裝優雅,API 友好,原始碼註釋明確,快速靈活,容錯方便
- 優勢:
- 對於 golang 而言,web 框架的依賴要遠比 Python,Java 之類的要小
- 自身的 net/http 足夠簡單,效能也非常不錯
- 藉助框架開發,不僅可以省去很多常用的封裝帶來的時間,也有助於團隊的編碼風格和形成規範
(2)安裝
Go 語言基礎以及 IDE 配置可以參考《Go | 部落格園-SRIGT》
-
使用命令
go get -u github.com/gin-gonic/gin
安裝 Gin 框架 -
在專案根目錄新建 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.Context
的Params
方法獲取引數 -
舉例
-
修改 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/ 之後的所有內容 -
訪問 http://localhost:8000/SRIGT/studying 檢視頁面
-
b. URL
-
可以透過
DefaultQuery
方法或Query
方法獲取資料- 區別在於當引數不存在時:
DefaultQuery
方法返回預設值,Query
方法返回空串
- 區別在於當引數不存在時:
-
舉例
-
修改 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") }
-
訪問 http://localhost:8000/?name=SRIGT&age=18 和 http://localhost:8000/?age= 檢視頁面
-
c. 表單
-
表單傳輸為 POST 請求,HTTP 常見的傳輸格式為四種
- application/json
- application/x-www-form-urlencoded
- application/xml
- multipart/form-data
-
表單引數可以透過
PostForm
方法獲取,該方法預設解析 x-www-form-urlencoded 或 form-data 格式的引數 -
舉例
-
在專案根目錄新建 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>
-
修改 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") }
-
使用瀏覽器開啟 index.html,填寫表單並點選按鈕提交
-
(3)檔案上傳
a. 單個
-
multipart/form-data 格式用於檔案上傳
-
檔案上傳與原生的 net/http 方法類似,不同在於 Gin 把原生的
request
封裝到context.Request
中 -
舉例
-
修改 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>
-
修改 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") }
-
使用瀏覽器開啟 index.html,選擇檔案並點選按鈕提交
-
修改 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") }
-
重新整理頁面,選擇檔案並點選按鈕提交
-
b. 多個
-
修改 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>
-
修改 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") }
-
使用瀏覽器開啟 index.html,選擇多個檔案並點選按鈕提交
(4)路由組
-
路由組(routes group)用於管理一些相同的 URL
-
舉例
-
修改 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)) }
-
使用 Postman 對以下連結測試 GET 或 POST 請求
- http://localhost:8000/v1/login
- http://localhost:8000/v1/submit
- http://localhost:8000/v2/login
- 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. 拆分成獨立檔案
- 當路由條目更多時,將路由部分拆分成一個獨立的檔案或包
拆分成獨立檔案
-
在下面根目錄新建 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 }
-
修改 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) } }
拆分成包
-
新建 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
-
修改 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. 拆分成多個檔案
- 當路由條目更多時,將路由檔案拆分成多個檔案,此時需要使用包
-
在 ~/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) }
-
-
修改 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
-
~/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) }
-
~/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) }
-
~/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
函式來進行路由的初始化操作
- 定義
-
修改 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
-
修改 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") }
-
使用 Postman 模擬客戶端傳參(body/raw/json)
(2)表單
-
修改 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>
-
修改 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") }
-
使用瀏覽器開啟 index.html,填寫表單並點選按鈕提交
(3)URI
-
修改 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") }
-
訪問 http://localhost:8000/login/root/admin 檢視頁面
0x04 渲染
(1)資料格式的響應
-
JSON、結構體、XML、YAML 類似於 Java 中的
properties
、ProtoBuf
-
舉例
-
JSON
route.GET("/", func(context *gin.Context) { context.JSON(http.StatusOK, gin.H{"message": "JSON"}) })
-
結構體
route.GET("/", func(context *gin.Context) { var msg struct{ Message string } msg.Message = "Struct" context.JSON(http.StatusOK, msg) })
-
XML
route.GET("/", func(context *gin.Context) { context.XML(http.StatusOK, gin.H{"message": "XML"}) })
-
YAML
route.GET("/", func(context *gin.Context) { context.YAML(http.StatusOK, gin.H{"message": "YAML"}) })
-
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
-
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>{{ .title }}</title> </head> <body> Content: {{ .content }} </body> </html>
-
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") }
-
訪問 http://localhost:8000/ 檢視頁面
b. 子模板
目錄結構:
graph TB 根目錄-->tem & main.go & go.mod tem-->page-->index.html
-
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 }}
-
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") }
-
訪問 http://localhost:8000/ 檢視頁面
c. 組合模板
目錄結構:
graph TB 根目錄-->tem & main.go & go.mod tem-->page & public public-->header.html & footer.html page-->index.html
-
header.html
{{ define "public/header" }} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{ .title }}</title> </head> <body> {{ end }}
-
footer.html
{{ define "public/footer" }} </body> </html> {{ end }}
-
index.html
{{ define "page/index.html" }} {{ template "public/header" }} Content: {{ .content }} {{ template "public/footer" }} {{ end }}
-
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") }
-
訪問 http://localhost:8000/ 檢視頁面
(3)重定向
-
修改 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") }
-
訪問 http://localhost:8000/ 檢視頁面
(4)同步與非同步
goroutine
機制可以實現非同步處理- 啟動新的
goroutine
時,不應該使用原始上下文,必須使用它的副本
-
修改 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") }
-
訪問 http://localhost:8000/ 檢視頁面
0x05 中介軟體
(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)Next 方法
Next()
是一個控制流的方法,它決定了是否繼續執行後續的中介軟體或路由處理函式
-
修改 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) } } // ...
-
執行
(3)區域性中介軟體
-
修改 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") }
-
執行
0x06 會話控制
(1)Cookie
a. 概述
- 簡介:Cookie 實際上就是伺服器儲存在瀏覽器上的一段資訊,瀏覽器有了 Cookie 之後,每次向伺服器傳送請求時都會同時將該資訊傳送給伺服器,伺服器收到請求後,就可以根據該資訊處理請求 Cookie 由伺服器建立,併傳送給瀏覽器,最終由瀏覽器儲存
- 缺點:採用明文、增加頻寬消耗、可被禁用、存在上限
b. 使用
-
測試服務端傳送 Cookie 給客戶端,客戶端請求時攜帶 Cookie
-
修改 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 布林型
-
訪問 http://localhost:8000/,此時輸出 “Cookie: NotSet”
-
重新整理頁面,此時輸出 “Cookie: value_cookie”
-
-
模擬實現許可權驗證中介軟體
說明:
- 路由 login 用於設定 Cookie
- 路由 home 用於訪問資訊
- 中介軟體用於驗證 Cookie
-
修改 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 } }
-
依次訪問以下頁面
- http://localhost:8000/home
- http://localhost:8000/login
- http://localhost:8000/home
- 等待 60 秒後重新整理頁面
(2)Sessions
- gorilla/sessions 為自定義 Session 後端提供 Cookie 和檔案系統 Session 以及基礎結構
-
修改 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") }
-
依次訪問以下頁面
- http://localhost:8000/get
- http://localhost:8000/set
- http://localhost:8000/get
- http://localhost:8000/del
- http://localhost:8000/get
0x07 引數驗證
(1)結構體驗證
- 使用 Gin 框架的資料驗證,可以不用解析資料,減少
if...else
,會簡潔很多
-
修改 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") }
-
訪問 http://localhost:8000/?name=SRIGT&age=18 檢視頁面
(2)自定義驗證
- 對繫結解析到結構體上的引數,自定義驗證功能
- 步驟分為
- 自定義校驗方法
- 在
binding
中使用自定義的校驗方法函式註冊的名稱 - 將自定義的校驗方法註冊到
validator
中
-
修改 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") }
-
依次訪問以下頁面
- http://localhost:8000/?age=18
- http://localhost:8000/?name=admin&age=18
- http://localhost:8000/?name=SRIGT&age=18
(3)多語言翻譯驗證
舉例:返回資訊自定義,手機端返回的中文資訊,pc 端返回的英文資訊,需要做到請求一個介面滿足上述三種情況
-
修改 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)) }
-
依次訪問以下頁面
- http://localhost:8000/?str1=abc&str2=def&str3=ghi&locale=zh
- http://localhost:8000/?str1=abc&str2=def&str3=ghi&locale=en
0x08 其他
(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") }
-
執行後,檢視檔案 gin.log
(2)Air 實時載入
a. 概述
- Air 能夠實時監聽專案的程式碼檔案,在程式碼發生變更之後自動重新編譯並執行,大大提高 Gin 框架專案的開發效率
- 特性:
- 彩色的日誌輸出
- 自定義構建或必要的命令
- 支援外部子目錄
- 在 Air 啟動之後,允許監聽新建立的路徑
- 更棒的構建過程
b. 安裝與使用
參考 Air 倉庫:https://github.com/cosmtrek/air
(3)驗證碼
-
驗證碼一般用於防止某些介面被惡意呼叫
-
實現步驟
- 提供一個路由,在 Session 中寫入鍵值對,並將值寫在圖片上,傳送到客戶端
- 客戶端將填寫結果返送給服務端,服務端從 Session 中取值並驗證
-
舉例
-
修改 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>
-
修改 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") }
-
依次訪問以下頁面
- 獲取驗證碼圖片:http://localhost:8000/captcha
- 提交結果並驗證:http://localhost:8000/captcha/verify/xxxx
-
(4)生成解析 token
-
有很多將身份驗證內建到 API 中的方法,如 JWT(JSON Web Token)
-
舉例:獲取 JWT,檢查 JWT
-
修改 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 }
-
依次訪問以下頁面
- http://localhost:8000/get
- http://localhost:8000/set
- http://localhost:8000/get
-
-End-