📦 Go1.22.5
、Gin1.10.0
⏮ 前置课程 httpRouter
🤦下面这些知识都是我东拼西凑找教程学的,官方网站写的不太可读,b站上的课程就只是教你怎么用
🌟 请求参数和路由处理
⭐️ Gin
的路由组件是使用的 httpRouter
,可以学一下 httprouter
⭐️ Get
请求的 Query
就是比如 http://httpbin.org/get?name=xxxx
这种样式
⭕️ 有两种方法
Query
需要自己输入值DefaultQuery
带默认值
1️⃣将此前是 someGet
方法修改一下,给他添加一个 Query
,在使用 String
去处理 Query
,现在只演示 valueQ
的
func someGet(context *gin.Context) {
valueQ := context.Query("name")
valueDQ := context.DefaultQuery("age", "tanc")
//返回一个字符串
context.String(200, "Hello %s", valueQ)
}
2️⃣ 会发现 Query
加上去之后就会显示 Query
的值,如果不加是不会显示的
3️⃣ 试一下 DefaultQuery
,修改代码
func someGet(context *gin.Context) {
valueQ := context.Query("name")
valueDQ := context.DefaultQuery("age", "tanc")
//返回一个字符串
context.String(200, "Query: Hello %s", valueQ)
context.String(200, "DefaultQuery: Hello %s", valueDQ)
}
4️⃣ 即使没有添加 Query
,添加了默认值
⭐️Post
的 Form
表单数据
也就是提交的表单上下文
⭕️ 也是主要有两个方法
Posrform
需要自己输入DefaultPostForm
默认参数
1️⃣ 将 somePost
方法修改一下
func somePost(context *gin.Context) {
formQ := context.PostForm("name")
formDQ := context.DefaultPostForm("age", "tanc")
context.JSON(200, gin.H{
"message": "somPost",
"PostForm": formQ,
"DefaultPostForm": formDQ,
})
}
2️⃣ 发送请求
⭐️ Json请求模板
有三种方法
🍵 第一种方法,使用空结构体接收处理,
1️⃣ 通过创建一个 map
,map
表示 key
为 string
,value
可以为任意类型,然后使用 BindJson
绑定这个 json
func main(){
....
r.GET("/someJson", somJson)
....
}
func somJson(context *gin.Context) {
json := make(map[string]interface{})
context.BindJSON(&json)
context.JSON(200, gin.H{
"message": "somJson",
"name": json["name"],
"password": json["password"],
})
}
2️⃣ 在来查看一下 JSON
方法接收的参数,接收一个 code
状态码,然后是任何对象
func (c *Context) JSON(code int, obj any) {
c.Render(code, render.JSON{Data: obj})
}
3️⃣ 再来看 gin.H
,它 map[string]interface{}
差不多
// H is a shortcut for map[string]any
type H map[string]any
4️⃣ 来测试一下
🍵 第二种方法: 定义结构体
1️⃣ 定义一个需要接收的结构体,这样很像 SpringBoot
的 model
,这个很像我学 http/net
包的时候,的 json
,这个后面内容表示 json
接收的 key
值
// User 接收json结构体
type User struct {
Name string `json:"name"`
Password string `json:"password"`
}
2️⃣ 在创建一个路由
func main() {
//创建gin server服务
r := gin.Default()
...
r.GET("/structJson", structJson)
...
//启动服务,默认是8080端口
r.Run(":8082")
}
3️⃣ 创建方法
func structJson(context *gin.Context) {
json := User{}
context.BindJSON(&json)
context.JSON(200, gin.H{
"message": "somJson",
"name": json.Name,
"password": json.Password,
})
}
4️⃣ 测试
🍵第三种方法:数据绑定表单 FormBind
1️⃣ 通过 ShouldBind
来绑定结构体变量 ,首先需要新建一个结构体,注意结构体后面必须是 form
而不是 json
如果你是绑定复选框,那么就直接写一个数组即可
// Model 表单数据绑定接收的结构体
type Model struct {
Name string `form:"name"`
Password string `form:"password"`
// 绑定复选框
Code []string `form:"code[]"`
}
2️⃣ 添加新的路由,由于是表单形式,所以需要 Post
r.POST("/FormBind", formBind)
3️⃣ 编写路由的方法,使用 ShouldBind
绑定数组
func formBind(context *gin.Context) {
var model Model
context.ShouldBind(&model)
context.String(200, "FormBind: Hello %s %s 常用编程语言: %s\n", model.Name, model.Password, getCode(model))
}
//格式化数组
func getCode(model Model) string {
var s string
for _, v := range model.Code {
s += v + " "
}
//去除最后一空格
s = strings.TrimSpace(s)
return s
}
4️⃣ 编写前端表单,注意这里还是使用之前的 index
页面
<form action="/FormBind" method="post">
<input type="text" name="name">
<input type="text" name="password">
<input type="checkbox" name="code[]" value="Golang">Golang<br>
<input type="checkbox" name="code[]" value="Python">Python<br>
<input type="checkbox" name="code[]" value="Java">Java<br>
<input type="submit" value="submit">
</form>
5️⃣ 测试
点击 submit
还有一种方法,这种方法是 binding:"required"
。这意味着当 Gin 尝试从HTTP请求的表单数据中绑定这些字段时,这两个字段必须存在且非空。如果不满足条件,数据绑定将会失败,Gin 可能会返回一个错误响应,指出哪些字段是必填的。
// Model 表单数据绑定接收的结构体
type Model struct {
Name string `form:"name" binding:"required"`
Password string `form:"password" binding:"required"`
// 绑定复选框
Code []string `form:"code[]"`
}
func formBind(context *gin.Context) {
var model Model
if context.ShouldBind(&model) == nil {
if model.Name == "tanc" && model.Password == "123456" {
context.JSON(200, gin.H{
"status": "hello tanc!!",
})
}else {
context.JSON(400, gin.H{
"status": "请输入正确用户",
})
}
}
}
☑️ 拓展 Json
此外还有两种拓展的 Json
:
PureJson
JSON
使用unicode
替换特殊HTML
字符,例如<
变为\ u003c
。如果要按字面对这些字符进行编码,则可以使用PureJSON
SecureJson
: 防止json
劫持。如果给定的结构是数组值,则默认预置"while(1),"
到响应体头部来防止劫持,如果你的响应体不是数组,那么这个前缀不会被自动添加,但在任何情况下,SecureJSON
都会确保输出的JSON
符合安全规范,减少JSON
劫持的风险。AsciiJson
: 生成具有转义的非ASCII
字符的ASCII-only JSON
,当一个JSON需要是纯ASCII的,即可以被ASCII字符集兼容的系统正确处理时,所有非ASCII字符都会被这样转义
⭕️ PureJson
1️⃣ 首先介绍第一种 Json
,它和普通 Json
一样接收两个参数
func (c *Context) PureJSON(code int, obj any) {
c.Render(code, render.PureJSON{Data: obj})
}
2️⃣ 编写代码方式也是一样,可以填写 html
代码 <h1>
检测是否会被替换成 Unicode
代码
main(){
...
r.GET("/PureJson", pureJson)
...
}
func pureJson(context *gin.Context) {
context.PureJSON(200, gin.H{
"message": "<h1>This is my first Gin code<h1>",
})
}
3️⃣ 测试,并没有替换成 Unicode
代码,可以替换成普通 Json
测试一下
⭕️ SecureJson
1️⃣ 同样首先查看一下源代码
func (c *Context) SecureJSON(code int, obj any) {
c.Render(code, render.SecureJSON{Prefix: c.engine.secureJSONPrefix, Data: obj})
}
2️⃣ 编写代码,首先测试一个如果给定的结构是数组
func main() {
r := gin.Default()
// 你也可以使用自己的 SecureJSON 前缀
// r.SecureJsonPrefix(")]}',\n")
r.GET("/someJSON", func(c *gin.Context) {
names := []string{"lena", "austin", "foo"}
// 将输出:while(1);["lena","austin","foo"]
c.SecureJSON(http.StatusOK, names)
})
// 监听并在 0.0.0.0:8080 上启动服务
r.Run(":8080")
}
2️⃣ 浏览器测试
⭕️ AsciiJson
1️⃣ 编写代码
func main() {
r := gin.Default()
r.GET("/someJSON", func(c *gin.Context) {
data := map[string]interface{}{
"lang": "GO语言",
"tag": "<br>",
}
// 输出 : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
c.AsciiJSON(http.StatusOK, data)
})
// 监听并在 0.0.0.0:8080 上启动服务
r.Run(":8080")
}
⭐️ HTML
模板渲染
可以实现类似 Django
,tmplates
的功能
⭕️ 有两种方法导入 Html
文件
LoadHTMLGlob
导入全部LoadHTMLFiles
传入单个
1️⃣ 两个方法都大差不差的,首先在你的代码目录创建一个 templates/html
文件夹,用来保存 html
文件
2️⃣ 创建新的路由,和处理方法
//HTML模板渲染,注意路径
r.LoadHTMLGlob("D:/learnCode/Go/GinStudy/firstCode/templates/html/*")
//或者
//r.LoadHTMLFiles("templates/html/index.html")
r.GET("/index", someHtml)
这里需要使用 HTML
方法,它接收3个参数,分别是:状态码,html
文件名、需要传递过去的值,
func someHtml(context *gin.Context) {
context.HTML(http.StatusOK, "index.html", gin.H{
"Hello": "This is first Gin code",
})
}
3️⃣ 修改 html
文件,类似于 Django
的 context
,可以在 html
中接收,这里使用 .Hello
接收,名字需要和函数中定义的一样
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>This is first Gin code</title>
</head>
<body>
{{.Hello}}
</body>
</html>
4️⃣ 打开浏览器访问
⭐️ 绑定路由
使用 ShouldBindUri
来绑定 uri
,这里的 uri
是路由,不是
1️⃣ 同样创建结构体接收 uri
的,这个 uri:"id" binding:"required,uuid"
表示这个绑定到 id
,是必须的,且需要按照 uuid
格式
// UriModel 绑定Uri的Model
type UriModel struct {
Name string `uri:"name" binding:"required"`
ID string `uri:"id" binding:"required,uuid"`
}
2️⃣ 创建新路由和,路由处理 :name
表示根据路由捕获,使用了 httprouter
r.GET("/:name/:id", bindUri)
3️⃣ 判断是否捕获到或者捕获到的 uri
按照要球
func bindUri(context *gin.Context) {
var uri UriModel
if err := context.ShouldBindUri(&uri); err != nil {
context.JSON(200, gin.H{
"msg": err.Error(),
})
return
}
context.JSON(200, gin.H{
"name": uri.Name,
"id": uri.ID,
})
}
4️⃣ 测试
输入 UUID
不按照 uuid
格式
⭐️ binding
内置规则
前面也提到了使用 binding
校验器来 uuid
,它还提供了很多校验,他需要配置 Shouldxxx
使用,在Gin框架中,数据绑定和验证主要依赖于 github.com/go-playground/validator/v10
库,如果数据验证失败,Gin会利用 validator
这个库进行错误检查
// 不能为空,并且不能没有这个字段
required: 必填字段,如:binding:"required"
// 允许为空
omitempty
// 针对字符串的长度
min 最小长度,如:binding:"min=5"
max 最大长度,如:binding:"max=10"
len 长度,如:binding:"len=6"
// 针对数字的大小
eq 等于,如:binding:"eq=3"
ne 不等于,如:binding:"ne=12"
gt 大于,如:binding:"gt=10"
gte 大于等于,如:binding:"gte=10"
lt 小于,如:binding:"lt=10"
lte 小于等于,如:binding:"lte=10"
// 针对同级字段的
eqfield 等于其他字段的值,如:PassWord string `binding:"eqfield=Password"`
nefield 不等于其他字段的值
- 忽略字段,如:binding:"-" 或者不写
// 枚举 只能是red 或green
oneof=red green
// 字符串
contains=fengfeng // 包含fengfeng的字符串
excludes // 不包含
startswith // 字符串前缀
endswith // 字符串后缀
// 数组
dive // dive后面的验证就是针对数组中的每一个元素
// 网络验证
ip
ipv4
ipv6
uri
url
// uri 在于I(Identifier)是统一资源标示符,可以唯一标识一个资源。
// url 在于Locater,是统一资源定位符,提供找到该资源的确切路径
// 日期验证 1月2号下午3点4分5秒在2006年
datetime=2006-01-02
//其他校验
email 邮箱
uuid uuid校验
😄 当然也可以自己编写校验规则,使用 binding.Validator.Engine().(*validator.Validate)
这段代码获取 Gin 框架默认使用的 validator
库的实例,并通过类型断言确保可以安全地将其当作 *validator.Validate
类型来使用,如果可以就使用 RegisterValidation
注册一个验证器名为 sign
,实现函数为 singValid
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("sign", signValid)
}
🍵 将 binding
错误信息装换为中文
😢 将错误信息翻译成中文,主要是使用的 validator.ValidationErrors
它是检测 binding
错误类型的一个错误类型,然后将错误转换为中文,通过 RegisterTagNameFunc
来获取标签
/**
* @Author tanchang
* @Description //TODO
* @Date 2024/6/21 20:17
* @File: test
* @Software: GoLand
**/
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"net/http"
"reflect"
"strings"
)
var trans ut.Translator
func init() {
// 创建翻译器
uni := ut.New(zh.New())
trans, _ = uni.GetTranslator("zh")
// 自定义binding
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
_ = zh_translations.RegisterDefaultTranslations(v, trans)
}
//将字段名也切换为中文
v.RegisterTagNameFunc(func(field reflect.StructField) string {
//获取结构体中定义的label信息
label := field.Tag.Get("label")
//如果label为空则直接返回字段名
if label == "" {
return field.Name
}
return label
})
}
// 公共方法,将错误信息翻译成中文
func validateErr(err error) string {
// 判断是否为validator.ValidationErrors错误,如果是验证错误,则调用翻译器,
errs, ok := err.(validator.ValidationErrors)
// 如果不是验证错误,则直接返回错误信息
if !ok {
return err.Error()
}
//创建list存储翻译器翻译后的错误信息
var list []string
// 遍历错误列表,调用翻译器翻译,因为ValidationErrors放回的是一个切片所以需要遍历
for _, e := range errs {
list = append(list, e.Translate(trans))
}
// 将list拼接成字符串返回
return strings.Join(list, ";")
}
type User struct {
Name string `json:"name" binding:"required" label:"用户名"`
Email string `json:"email" binding:"required,email" label:"邮箱"`
}
func main() {
r := gin.Default()
// 注册路由
r.POST("/user", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
// 参数验证失败,调用翻译器
c.String(200, validateErr(err))
return
}
// 参数验证成功
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Hello, %s! Your email is %s.", user.Name, user.Email),
})
})
// 启动HTTP服务器
r.Run()
}
修改版本,改成 json
格式,通过设置一个分隔符来分割达到 json
效果
/**
* @Author tanchang
* @Description //TODO
* @Date 2024/6/21 20:17
* @File: test
* @Software: GoLand
**/
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"net/http"
"reflect"
"strings"
)
var trans ut.Translator
func init() {
// 创建翻译器
uni := ut.New(zh.New())
trans, _ = uni.GetTranslator("zh")
// 注册翻译器
v, ok := binding.Validator.Engine().(*validator.Validate)
if ok {
_ = zh_translations.RegisterDefaultTranslations(v, trans)
}
//将字段名也切换为中文,filed就是创建的对象,可以操作这个对象拿取tag
v.RegisterTagNameFunc(func(field reflect.StructField) string {
//获取结构体中定义的label信息
label := field.Tag.Get("label")
//如果label为空则直接返回字段名
if label == "" {
label = field.Name
}
//拿取json字段
name := field.Tag.Get("json")
return fmt.Sprintf("%s----%s", name, label)
})
}
// 公共方法,将错误信息翻译成中文
func validateErr(err error) any {
// 判断是否为validator.ValidationErrors错误,如果是验证错误,则调用翻译器,
errs, ok := err.(validator.ValidationErrors)
// 如果不是验证错误,则直接返回错误信息
if !ok {
return err.Error()
}
//创建list存储翻译器翻译后的错误信息
m := map[string]any{}
for _, e := range errs {
// 获取翻译器翻译后的错误信息
msg := e.Translate(trans)
//通过冒号分割,获取字段名
split := strings.Split(msg, "----")
//存储
m[split[0]] = split[1]
return m
}
}
type User struct {
Name string `json:"name" binding:"required" label:"用户名"`
Email string `json:"email" binding:"required,email" label:"邮箱"`
}
func main() {
r := gin.Default()
// 注册路由
r.POST("/user", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
// 参数验证失败,调用翻译器
c.JSON(200, validateErr(err))
return
}
// 参数验证成功
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Hello, %s! Your email is %s.", user.Name, user.Email),
})
})
// 启动HTTP服务器
r.Run()
}
最终版本测试
🍵 自定义验证器
🍅 这里我使用官方示例
/**
* @Author tanchang
* @Description //TODO
* @Date 2024/6/21 23:33
* @File: Gin官网示例代码之自定义验证器
* @Software: GoLand
**/
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
// Booking 包含绑定和验证的数据。
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"`
}
// 表示创建一个validator.Func的bookableDate的变量,validator.Func需要接收一个函数
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
//Field返回一个Value对象,然后使用Interface()方法获取,通过断言转换为time.Time类型
date, ok := fl.Field().Interface().(time.Time)
if ok {
//获取当前时间
today := time.Now()
//判断当前时间是否大于传入的时间
if today.After(date) {
return false
}
}
return true
}
func main() {
route := gin.Default()
//注册验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 使用RegisterValidation注册自定义验证器名为bookabledate,实现方法为bookableDate
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable)
route.Run(":8085")
}
func getBookable(c *gin.Context) {
var b Booking
//和ShouldBindQuery一样的功能
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
1️⃣ 一步一步来看,先查看结构体
time.Time
: 表明该字段存储的是一个时间点。form:"check_in"
: 表示在表单数据中,这个字段对应于check_in
键。binding:"required,bookabledate"
required
: 表示该字段是必填的,如果缺失则验证失败。bookabledate
: 应用了之前定义的自定义验证规则bookableDate
,确保日期是可预订的(即日期在未来)。gtfield=CheckIn
: 这是一个比较验证规则,要求CheckOut
字段的值必须大于(在时间意义上)CheckIn
字段的值,即退房日期必须在入住日期之后。
time_format:"2006-01-02"
: 指定了日期字符串的预期格式,遵循Go的日期时间格式化约定,这里表示年月日格式(例如,2023-04-01)。
// Booking 包含绑定和验证的数据。
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"`
}
2️⃣ 在查看 main
函数,自定义了一个验证器
func main() {
route := gin.Default()
//注册验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 使用RegisterValidation注册自定义验证器名为bookabledate,实现方法为bookableDate
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable)
route.Run(":8085")
}
3️⃣ 查看自定义验证器内容
// 表示创建一个validator.Func的bookableDate的变量,validator.Func需要接收一个函数
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
//Field返回一个Value对象,然后使用Interface()方法获取,通过断言转换为time.Time类型
date, ok := fl.Field().Interface().(time.Time)
if ok {
//获取当前时间
today := time.Now()
//判断当前时间是否大于传入的时间
if today.After(date) {
return false
}
}
return true
}
这里其实可以简化,它加
validator.Func
是为了告诉你它是一个自定义验证函数,其实不加也可以你只需在注册中添加这个函数即可
使用
fl.Field().Interface()
调用 fl.Field()
方法会返回一个 reflect.Value
类型的对象。reflect.Value
是反射(Reflection
)机制中的核心类型,它代表了运行时对象的值,然后将它转换为一个空接口,方便转换为 time.Time
类型
// 表示创建一个validator.Func的bookableDate的变量,validator.Func需要接收一个函数
func bookableDate(fl validator.FieldLevel) bool {
//Field返回一个Value对象,然后使用Interface()方法获取,通过断言转换为time.Time类型
date, ok := fl.Field().Interface().(time.Time)
if ok {
//获取当前时间
today := time.Now()
//判断当前时间是否大于传入的时间
if today.After(date) {
return false
}
}
return true
}
Filed()
源代码
func (v *validate) Field() reflect.Value {
return v.flField
}
4️⃣ 请求代码
func getBookable(c *gin.Context) {
var b Booking
//和ShouldBindQuery一样的功能
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
4️⃣ 测试
⭐️ 路由组
1️⃣它作用是在你要请求的路由如果是在同一个页面或者是功能是差不多可以使用路由组在这些路由前面加一个统一的前缀(因人使用)
/**
* @Author tanchang
* @Description //TODO
* @Date 2024/6/22 16:03
* @File: main.go
* @Software: GoLand
**/
package main
import "github.com/gin-gonic/gin"
func main() {
ginServer := gin.New()
ginServer.Use(gin.Logger(), gin.Recovery())
//定义路由组
v1 := ginServer.Group("/v1")
{
v1.GET("/home", homeHandleV1)
}
v2 := ginServer.Group("/v2")
{
v2.GET("/home", homeHandleV2)
}
ginServer.Run(":8083")
}
func homeHandleV1(context *gin.Context) {
context.String(200, "Yes V1!!!")
}
func homeHandleV2(context *gin.Context) {
context.String(200, "Yes V2!!!")
}
2️⃣ 测试:
3️⃣ Group
和其他请求方法一样可以使用处理器,看一下它的源代码
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
添加一个处理器,它会显示在 home
路由器前面
v1 := ginServer.Group("/v1", func(context *gin.Context) {
context.JSON(200, gin.H{
"Test": 123456,
})
})
{
v1.GET("/home", homeHandleV1)
}
测试: