Gin
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/ 之后的所有内容
-
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 请求
-
(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") }
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 } }
-
依次访问以下页面
(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") }
-
依次访问以下页面
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") }
(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") }
-
依次访问以下页面
(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)) }
-
依次访问以下页面
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. 安装与使用
- 使用命令
go install github.com/cosmtrek/air@latest
安装最新版的 Air - 在 GOPATH/pkg/mod/github.com/cosmtrek/air 中,将 air.exe 文件复制到 GOROOT/bin 中
- 如果没有 air.exe 文件,可以使用命令
go build .
生成 air.exe
- 如果没有 air.exe 文件,可以使用命令
- 使用命令
air -v
确认 Air 是否安装成功 - 在项目根目录下,使用命令
air init
生成 Air 配置文件 .air.toml - 使用命令
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") }
-
依次访问以下页面
-
(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 }
-
依次访问以下页面
-
-End-