Gin 是一个Go (Golang) 编写的轻量级 http web 框架,运行速度非常快,如果你是性能和高效的追求者,我们推荐你使用Gin 框架。
Gin 最擅长的就是Api 接口的高并发,如果项目的规模不大,业务相对简单,这个时候我们也推荐您使用Gin。
当某个接口的性能遭到较大挑战的时候,这个还是可以考虑使用Gin 重写接口。
Gin 也是一个流行的 golang Web 框架,Github Strat 量已经超过了50k。
Gin 的官网:https://gin-gonic.com/zh-cn/
Gin Github 地址:https://github.com/gin-gonic/gin
下载gin
$ go get -u github.com/gin-gonic/gin
为方便后续调试,可以下载postman,也可以在线使用,官网 :Postman API Platform
一.hello world
代码
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
//创建一个默认路由
router := gin.Default()
//绑定路由规则和路由函数,访问/index的路由,将由对应的函数(get)去处理
router.GET("/index", func(context *gin.Context) {
context.String(200, "hello world")
})
//启动监听,gin会把web服务运行在本机的0.0.0.0:8080端口
router.Run(":8080")
}
解释:
gin.default 使用gin的default方法创建一个路由handler,后续可以绑定各类路由规则和函数,它会在初始化阶段就引入 Logger 和 Recovery 中间件,能够保障你应用程序的最基本运作,这两个中间件具有以下作用:
- Logger:输出请求日志,并标准化日志的格式。
- Recovery:异常捕获,也就是针对每次请求处理进行 recovery 处理,防止因为出现 panic 导致服务崩溃,并同时标准化异常日志的格式。
router.get 创建不同的http方法绑定到handlers中
gin.context 是对net/http包中request和response的封装。是gin中的上下文,允许我们在中间件之间传递变量,管理流,验证json请求,相应json请求等
最后启动路由的run方法监听端口,和http.listenandserver本质是一样的,两种启动方式
router.run(":8080")
http.listenandserver(":8080",router)
结果
补充知识
路由
路由(Routing)是由一个URI(或者叫路径)和一个特定的HTTP 方法(GET、POST 等)组成的,涉及到应用如何响应客户端对某个网站节点的访问。
RESTful API 是目前比较成熟的一套互联网应用程序的API 设计理论,所以我们设计我们的路由的时候建议参考RESTful API 指南。
在RESTful 架构中,每个网址代表一种资源,不同的请求方式表示执行不同的操作:
在Web开发中,"路由"是指将请求的URL转发到相应的处理程序或控制器的过程。路由通常用于将URL映射到特定的处理函数,以便根据请求动态生成响应内容。处理函数和URL的关系
为什么要使用路由?
传统web开发是每一个请求地址都会请求服务器来进行处理,但是用户有些操作则无需请求服务器,直接页面端修改下逻辑就能达到目的,这种最好使用路由,也许题主会有疑问:直接使用js处理下不就行了。使用js直接处理这些是可以的,事实上以前我们也这么做,但是这样做不便于用户收藏当前页,因为使用js时并不更新url,但是使用路由时,url也是随着改变的,用户浏览到一个网页时可以直接复制或收藏当前页的url给别人,这种方式对于搜索引擎和用户来说都是友好的。
ip地址问题
- 在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务。在局域网中可以多主机访问
- 在路由中,0.0.0.0表示的是默认路由,即当路由表中没有找到完全匹配的路由的时候所对应的路由
curl命令
curl
是一个命令行工具,用于向服务器发送 HTTP 请求并接收响应。可在Linux、Windows、Mac等系统中使用。
它支持多种协议,包括 HTTP、HTTPS、FTP、SMTP 等,并且可以通过命令行选项进行配置。
这里不用postman或浏览器,使用curl命令也可以发送get请求:
curl 127.0.0.1:8080/index
更多内容见:curl 命令的用法_咖啡与乌龙的博客-CSDN博客
二.响应
json响应
代码:
package main
import (
"github.com/gin-gonic/gin"
)
// 以string进行响应,返回string
func res_string(c *gin.Context) {
c.String(200, "ok")
}
// 以json进行响应,返回json
func res_json(c *gin.Context) {
//json响应结构体,同理也可以响应map,注意map是无序的,其内容先后顺序随机
type userjson struct {
Name string `json:"姓名"` //用备注来进行显示
Id int
Age int
Password string `json:"-"` //"-"代表这段不进行渲染,也可以使用password,不再包外进行使用来防止显示
}
jsonUser := userjson{"wang", 1, 12, "wqer"}
//响应后面的C.json去了。
c.JSON(200, jsonUser)
//从map中延申出去,这里即响应map,可以通过多次嵌套来方便json的书写
c.JSON(200, gin.H{"username": "fengfeng", "age": 18})
// gin.H是map[string]any即map[string]interface{}的缩写。
}
func main() {
e := gin.Default()
e.GET("/text", res_string)
e.GET("/json", res_json)
e.Run(":7000")
}
结构体大小写问题
结构体及其内容首字母需大写,否则包外读不到
//Student中的S大写可以在包外被访问,若小写只能在本包里被访问
type Student struct {
Name string // S大写的情况下可在包外访问的字段
class string // S大写的情况下仅限包内访问的字段
}
gin.h
引入 gin.H
这个东西可以简化生成 json 的方式,如果需要嵌套 json,那么嵌套 gin.H
就可以了。例子:
c.JSON(http.StatusOK, gin.H{
"status": gin.H{
"code": http.StatusOK,
"status": "登录成功",
},
"message": message
})
响应xml、yaml
// 响应xml
func res_xml(c *gin.Context) {
c.XML(200, gin.H{"name": "li", "major": gin.H{"grade": "fresh", "title": "machine"}})
}
// 响应yaml
func res_yaml(c *gin.Context) {
c.YAML(200, gin.H{"name": "li", "major": gin.H{"grade": "fresh", "title": "machine"}})
}
xml
yaml
响应html
在项目中新建文件夹templates,创建index.html
{{.XXX}}中xxx即为代码实际中要传入的参数,这里为username
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Title</title>
</head>
<body>
<header>你好{{.username}}</header>
</body>
</html>
html响应:
先要使用 LoadHTMLGlob()
或者LoadHTMLFiles()
方法来加载模板文件
package main
import (
"github.com/gin-gonic/gin"
)
// 响应html
func res_html(c *gin.Context) {
c.HTML(200, "index.html", gin.H{"username": "wang2"}) //注意这里的字段名需要与html模板文件中{{.XXX}}一致,否则无法传入
}
func main() {
e := gin.Default()
e.LoadHTMLGlob("templates/*") //html响应时,需要使用loadhtmlglob或loadhtmlfiles方法来加载模板文件
e.GET("/html", res_html)
e.Run(":9000")
}
文件响应
//配置单独的一个静态文件
// e.StaticFile("/static/Default.jpg", "/golang/gin/static/Default.jpg")//单独“/”代表从根目录进行路径识别,这里是从盘符开始
e.StaticFile("/static/Default.jpg", "./static/Default.jpg") //“./”即从当前目录开始寻径
// e.StaticFile("/static/Default.jpg", "static/Default.jpg")//什么都不写等同于“./”
//golang中没有相对文件的路径,只有相对项目的路径,即无论你在什么位置写代码,都是以项目为根目录
//网页请求某个静态目录
e.StaticFS("/newstatic", http.Dir("./static/static"))
出现在普通字符串中的路径,如果代表的是windows文件路径,则使用 斜杆/ 和 反斜杠\ 是一样的;如果代表的是网络文件路径,则必须使用 斜杆/ ;
重定向
// 重定向
func redirect(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com/")
}
func main(){
e := gin.Default()
e.GET("/baidu", redirect)
e.Run(":8000")
}
重定向状态码
301
被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。
302
请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。
请求
请求参数
查询参数
查询在url中输入包含的内容
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func query(c *gin.Context) {
//getquery方法获得传入的数据及是否有对应的关键词
fmt.Println(c.GetQuery("user"))
//query方法内调用getquery方法,获得传入的数据
fmt.Println(c.Query("user"))
fmt.Println(c.QueryArray("user")) //拿到多个相同的查询参数,返回切片类型
}
func main() {
e := gin.Default()
e.GET("/quest", query)
e.Run(":8080")
}
127.0.0.1:8080/quest?user=wang&user=zhaoyun
运行结果:
动态参数
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func parameter(c *gin.Context) {
fmt.Println(c.Param("user_id"))
fmt.Println(c.Param("book_id"))
}
func main() {
e := gin.Default()
e.GET("/param/:user_id/:book_id", parameter) //注意这里每个字符前都加:
e.Run(":8080")
}
动态参数和查询参数的区别
动态参数和查询参数都用于在 URL 中传递参数,但它们在使用方式和作用上有一些区别。
- 动态参数(Dynamic Route Parameters):
-
- 格式:动态参数被包裹在路由路径中,以冒号(:)开头,例如
/users/:id
。 - 作用:动态参数用于捕获 URL 中的可变部分,并将其作为参数传递给处理该路由的函数。它们通常用于标识资源的唯一标识符uri,例如用户 ID、文章 ID 等。
- 示例:在
/users/123
这个URL中,动态参数的名称是 "id",对应的值是 "123"。
- 格式:动态参数被包裹在路由路径中,以冒号(:)开头,例如
- 查询参数(Query Parameters):
-
- 格式:查询参数是以
?
开始,后面跟着键值对的形式,例如/search?q=apple&category=fruits
。 - 作用:查询参数用于向服务器传递额外的信息,可以有多个键值对。它们通常用于过滤、排序、分页等操作。
- 示例:在
/search?q=apple&category=fruits
这个 URL 中,查询参数有两个键值对:q
的值是 "apple",category
的值是 "fruits"。
- 格式:查询参数是以
表单参数
// 表单参数,使用post请求
func form(c *gin.Context) {
name := c.PostForm("name")
fmt.Println(name)
s := c.PostFormArray("name") //获取多个相同的表单参数,以切边输出
fmt.Printf("s: %v\n", s)
c.JSON(200, gin.H{"name_array": s, "name": name})
c.DefaultPostForm("city", "淄博") //如果用户没传,就使用默认值
fmt.Println(c.MultipartForm()) //接受所有的form表单,包括文件
}
func main() {
e := gin.Default()
e.POST("/list", form)
e.Run(":8003")
}
可以看到文件也传输到其中,为一接口
GET和POST请求的区别
简单来说,GET请求是通过URL参数传递数据,而POST请求则是通过request body传递数据。因此,GET请求通常用于数据量较小,不需要保密的场景;而POST请求则适用于传输大量数据或者需要保密的场景。由此可见,在处理敏感数据时使用POST比GET更加安全。
具体不同:GET请求不够优秀?为什么网页抓取大都选择POST? (baidu.com)
原始参数
通过post请求
// 原始参数
func raw(c *gin.Context) {
b, _ := c.GetRawData()
fmt.Printf("%v\n", string(b))
s := c.GetHeader(c.ContentType()) //获得header中的内容
fmt.Printf("s: %v\n", s)
}
form-data形式:
----------------------------854688213527707516473298
Content-Disposition: form-data; name="name"
wang
----------------------------854688213527707516473298
Content-Disposition: form-data; name="addr"
上海
----------------------------854688213527707516473298--
x-www-form-urlencodeed形式:
name=wang2&addr=%E6%B7%84%E5%8D%9A
json
c.ShouldBindJSON()
是 Gin 框架的一个方法,用于将请求的 JSON 数据绑定到一个结构体或映射上。
这个方法会根据请求的 Content-Type 头部来确定如何解析请求体。如果 Content-Type 是 "application/json",则会使用 JSON 解析器解析请求体数据,并将其绑定到指定的结构体或映射上。
// 原始参数
func raw(c *gin.Context) {
// b, _ := c.GetRawData()
// fmt.Printf("%v\n", string(b))
s := c.GetHeader("Content-Type") //获得header中的内容
fmt.Printf("s: %v\n", s)
switch s {
case "application/json":
type user struct { //通过增加备注可以使json中格式对应字段名,都一样也可以不备注
Name string `json:"name"`
Addr string `json:"city"`
Age int
}
var User user //结构体实例化
c.ShouldBindJSON(&User)
// 当然也可以通过json.Unmarshal方法来进行json到结构体的转化
fmt.Printf("user: %v\n", User)
}
}
func main() {
e := gin.Default()
e.POST("/raw", raw)
e.Run(":8020")
}
请求头中包含信息:
四大请求方式
GET POST PUT DELETE
Restful风格指的是网络应用中就是资源定位和资源操作的风格。不是标准也不是协议。
GET:从服务器取出资源(一项或多项)
POST:在服务器新建一个资源
PUT:在服务器更新资源(客户端提供完整资源数据)
PATCH:在服务器更新资源(客户端提供需要修改的资源数据)
DELETE:从服务器删除资源
具体选用哪一个,我们到时候根据接口文档选择即可
// 以文字资源为例
// GET /articles 文章列表
// GET /articles/:id 文章详情
// POST /articles 添加文章
// PUT /articles/:id 修改某一篇文章
// DELETE /articles/:id 删除某一篇文章
tip:同时编辑多行,vscode快捷键,alt+鼠标左键
restful风格的程序编写,仅供参考:
package main
//由于并非实际情况,函数中本应由数据库传输的内容由函数内给定
//简单演示
import (
"fmt"
"github.com/gin-gonic/gin"
)
// 定义文章结构体
type ArticleModel struct {
Title string `json:"title"`
Content string `json:"content"`
}
// 封装响应内容
type response struct {
Code int `json:"code"`
Data any
Msg string
}
// 文章列表页面
func getList(c *gin.Context) {
//自定义文章列表
articleList := []ArticleModel{
{"golang", "go语言入门内容"},
{"python", "python机器学习内容"},
{"java", "java后端内容"},
}
c.JSON(200, response{200, articleList, "文章列表"})
}
// 获取文章详情
func getDetail(c *gin.Context) {
//获取param中的id
fmt.Printf("c.Param(\"id\"): %v\n", c.Param("id"))
article := ArticleModel{"golang", "go语言入门内容"}
c.JSON(200, response{200, article, "文章详情"})
}
// 创建文章
func create(c *gin.Context) {
// 接受前端传递过来的json参数
var article ArticleModel
c.ShouldBindJSON(&article)
c.JSON(200, response{200, article, "创建文章"})
}
// 编辑文章
func update(c *gin.Context) {
//获取要修改的文章的id
c.Param("id")
//获取要修改的文章的内容
var article ArticleModel
c.ShouldBindJSON(&article)
c.JSON(200, response{200, article, "修改成功"})
}
// 删除文章
func delete(c *gin.Context) {
//获取要删除文章的id
c.Param("id")
//剩下的操作交给数据库
c.JSON(200, gin.H{})
}
func main() {
e := gin.Default()
e.GET("/articles", getList) //文章列表
e.GET("/articles/:id", getDetail) //文章详情
e.POST("/articles", create) //创建文章
e.PUT("articles/:id", update) //更新文章
e.DELETE("/articles/:id", delete) //删除文章
e.Run(":8040")
}
请求头、响应头相关.
什么是请求头
请求头(Request Headers)是 HTTP 请求中包含的一些元数据信息,用于向服务器传递额外的请求参数和属性。请求头通常由键值对(Key-Value pairs)组成,每个键值对表示一个请求头字段和对应的值。
以下是一些常见的请求头字段及其作用:
Host
:指定目标服务器的主机名和端口号。User-Agent
:发送请求的用户代理(浏览器、应用程序等)的标识信息。Content-Type
:指定请求体的媒体类型(如 application/json、application/x-www-form-urlencoded 等)。Content-Length
:指定请求体的长度。Authorization
:用于身份验证的凭据,常见的有基本认证(Basic Authentication)和 Bearer Token 认证。Accept
:指定客户端能够接受的响应内容类型。Cookie
:包含从服务器接收到的 Cookie 信息,用于保持会话状态。Referer
:指定当前请求的来源页面的 URL。
除了上述请求头字段,还有其他一些请求头字段可以用于传递各种类型的信息、控制缓存、跟踪会话等。不同的应用场景可能需要使用不同的请求头字段
什么是响应头
响应头(Response Headers)是在服务器向客户端发送HTTP响应时,包含在响应报文中的元数据信息。它们提供了关于响应的附加信息,帮助客户端理解和处理服务器返回的内容。
常见的响应头包括:
- Content-Type:指定响应体的媒体类型(例如,text/html、application/json等)。
- Content-Length:指定响应体的长度(以字节为单位)。
- Date:指定响应生成的日期和时间。
- Server:指定服务器软件的名称和版本号。
- Set-Cookie:设置一个或多个Cookie,用于在客户端保存会话信息。
- Cache-Control:指定响应的缓存行为,如缓存有效期或不缓存等。
- Location:指定重定向的目标URL。
package main
import (
"fmt"
"regexp"
"strings"
"github.com/gin-gonic/gin"
)
// 获得请求头的各种方法
func header(c *gin.Context) {
//不区分大小写,单词中间用 - 隔开
fmt.Printf("c.GetHeader(\"user-agent\"): %v\n", c.GetHeader("user-agent")) //getheader方法只能获取一个请求头
//header是一个MAP[string][]string类型数据
c.JSON(200, gin.H{"header": c.Request.Header}) // 获取header中的全部内容
//也可以用c.request.header.get()来获取单个值,自动匹配大小写
///通过c.request.header[xxx],即通过使用map查询的话。需要大小写完全一致
}
// 区别对待用户和爬虫的方法
func crawerDetect(c *gin.Context) {
s := c.GetHeader("user-agent")
//第一种方法是通过正则表达式进行匹配
//第二种方式是通过strings包的包含来进行匹配(正则校验)
if strings.Contains(s, "Postman") { //由于这里用的是postman,这里使用postman代替常用的python
//s中包含python字符,说明是爬虫
//返回空值
c.JSON(200, gin.H{"data": "爬虫给爷爬!"})
return
}
//正则校验的第一种方式
matched, _ := regexp.MatchString(`python`, "python") //regexp.matchstring第一个参数为正则表达式,第二个为校验的字符
if matched {
}
//正则校验的第二种方式
r := regexp.MustCompile("python") //首先通过正则表达式匹配构造regexp.ExpReg
if r.MatchString("python") { //通过上述的结构体的方法进行校验
}
//欢迎用户
c.JSON(200, gin.H{"data": "欢迎阿龙"})
regexp.MustCompile("python")
}
// 响应头设置
func responseHead(c *gin.Context) {
//用c.header以设置响应头
c.Header("Token", "jhgeu%hsg845jUIF83jh")
c.Header("Content-Type", "application/text; charset=utf-8")
c.JSON(0, gin.H{"data": "看看响应头"})
}
func main() {
e := gin.Default()
e.GET("/header", header)
e.GET("/crawer", crawerDetect)
e.GET("/response", responseHead)
e.Run(":8071")
}
请求头结果:
响应头结果
bind绑定参数
前面的 原始参数 部分我们其实已经使用过bind相关方法,其可以很方便地将 前端 传来的数据与 结构体 进行 参数绑定,以及 参数校验
mustbind方法一般不使用,失败可能会改变状态码
package main
import "github.com/gin-gonic/gin"
//定义结构体信息
type Userinfo struct { //json用于json绑定。form标记用于绑定query参数(其他)。uri绑定动态参数
Name string `json:"name" form:"name" uri:"name"`
Age int `json:"age" form:"age" uri:"age"`
Sex string `json:"sex" form:"sex" uri:"sex"`
}
//绑定json
func bindJson(c *gin.Context) {
//实例化结构体
var wang Userinfo
//将前端传来的json与wang绑定
c.ShouldBindJSON(&wang)
//以json响应输出wang以验证
c.JSON(200, gin.H{"json": wang})
}
//绑定查询参数
func bindQuery(c *gin.Context) {
//实例化结构体
var wang Userinfo
//将前端传来的query与wang绑定
c.ShouldBindQuery(&wang)
//响应输出wang以验证
c.JSON(200, gin.H{"json": wang})
}
//uri uniform resouce identifier 同一资源标识符,通常用于动态参数
func bindUri(c *gin.Context) {
//实例化结构体
var wang Userinfo
c.ShouldBindUri(&wang)
//响应输出wang以验证
c.JSON(200, gin.H{"json": wang})
}
//根据content-type进行匹配,例如form-data,tag用form
func bindAll(c *gin.Context) {
//实例化结构体
var wang Userinfo
c.ShouldBind(&wang)
//响应输出wang以验证
c.JSON(200, gin.H{"json": wang})
}
func main() {
e := gin.Default()
e.POST("/json", bindJson)
e.POST("/query", bindQuery)
e.POST("/uri/:name/:age/:sex", bindUri)
e.POST("/all", bindAll)
e.Run(":8072")
}
ShouldBindJson
ShouldBindQuery
ShoudBindUri
ShouldBind
验证器
bind绑定器(承接上一小节内容)
常用
// 不能为空,并且不能没有这个字段
required: 必填字段,如:binding:"required"
// 针对字符串的长度
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:"-"
实例:
必须输入姓名,密码必须一致,年龄在18-30
package main
import "github.com/gin-gonic/gin"
type SignUserInfo struct {
Name string `json:"name" binding:"required"`
Age int `json:"age" binding:"lt=30,gt=18" `
sex string `oneof:"woman man"`
Password string `json:"password"`
Repassword string `json:"repassword" binding:"eqfield=Password"` //注意这里绑定的是字段名
}
//验证
func verify(c *gin.Context) {
//实例化结构体
var wang SignUserInfo
//将内容绑定到结构体
err := c.ShouldBindJSON(&wang)
if err != nil {
c.JSON(200, gin.H{"meg": err.Error()})
return
}
c.JSON(200, "ok")
}
func main() {
e := gin.Default()
e.POST("/verify", verify)
e.Run(":8000")
}
gin内置验证器
代码扩充于上一节
package main
import "github.com/gin-gonic/gin"
type SignUserInfo struct {
Name string `json:"name" binding:"required"`
Age int `json:"age" binding:"lt=30,gt=18" `
Sex string `binding:"oneof=woman man"` //枚举 只能是woman或man
Password string `json:"password"`
Repassword string `json:"repassword" binding:"eqfield=Password"` //注意这里绑定的是字段名
Hobby []string `json:"like list" binding:"required,dive,required,startswith=like"` //从第dive后面开始就是针对数组内的内容进行校验
IP string `binding:"ip"`
Time string `binding:"datetime=2006-01"` //2006-01-02 15:04:05 是一个参考时间的格式,这里后面所跟一定要从参考时间格式截取
}
//验证
func verify(c *gin.Context) {
//实例化结构体
var wang SignUserInfo
//将内容绑定到结构体
err := c.ShouldBindJSON(&wang)
if err != nil {
c.JSON(200, gin.H{"meg": err.Error()})
return
}
c.JSON(200, "ok")
}
func main() {
e := gin.Default()
e.POST("/verify", verify)
e.Run(":8020")
}
常用内容
注意这些都是写在binding里面的内容
// 枚举 只能是red 或green
oneof=red green
// 字符串
contains=fengfeng // 包含fengfeng的字符串
excludes // 不包含
startswith // 字符串前缀
endswith // 字符串后缀
// 数组
dive // dive后面的验证就是针对数组中的每一个元素
// 网络验证
ip
ipv4
ipv6
uri
url
// uri 在于I(Identifier)是统一资源标示符,可以唯一标识一个资源。
// url 在于Locater,是统一资源定位符,提供找到该资源的确切路径
// 日期验证
datetime=2006-01-02
2006-01-02 15:04:05 是一个参考时间的格式,也就是其它语言中 Y-m-d H:i:s 格式,在功能上用于时间的格式化处理和规范。
那为什么是 2006-01-02 15:04:05 呢?其实这些 ”数字“ 是有意义的,每个布局字符串都是一个时间戳的表示,并非随便写的时间点。
可参见官方例子中的如下方式:
Jan 2 15:04:05 2006 MST
1 2 3 4 5 6 -7
自定义错误信息
为了使验证的错误信息更具有可读性。需要自定义错误信息,返回给前端
补充信息:
在golang中,.()被称为类型断言,可以将一个interface{}类型的变量转换成其它类型,例如字符串类型、整数类型等等。在调用函数时,我们可以使用.()将函数参数转换为不同的类型。
.()用法的基本语法如下:
value, ok := interface{}.(Type)
其中,value是转换后的值,ok是一个bool类型的返回值,表示转换是否成功。
下面是一个简单的例子,展示了如何使用.()将一个interface{}类型的变量转换成字符串类型:
GetValidMsg
函数的作用是从结构体的字段中获取 msg
参数,并返回错误信息。
函数签名为:
func GetValidMsg(err error, obj any) string
参数说明:
• err 是一个错误类型,用于接收验证错误信息。
• obj 是一个任意类型,需要传入结构体的指针。
函数内部实现的逻辑如下:
1. 通过 reflect.TypeOf(obj) 获取传入对象的具体类型。
2. 将 err 断言为 validator.ValidationErrors 类型,用于验证是否为验证错误。
3. 如果断言成功,则表示错误是由验证器返回的。
4. 遍历每一个错误信息,通过报错字段名获取结构体中对应的字段。
5. 如果找到了对应的字段,则通过 Tag.Get("msg") 获取字段的 msg 参数。
6. 返回 msg 参数作为错误提示信息。
7. 如果没有找到对应的字段,则返回原始错误信息 err.Error()。
内部方法解答:
1:
validator.ValidationErrors 是一个类型,它表示验证器返回的错误信息。
如果 err 是 validator.ValidationErrors 类型或其子类型,则转换成功, ok 的值将为 true,同时将转换后的结果赋值给 errs。
getObj.Elem().FieldByName(e.Field()) 这段代码用于根据报错字段名获取结构体的具体字段。
2:
• getObj 是一个 reflect.Type 类型的变量,表示传入对象的类型。
• getObj.Elem() 方法返回 getObj 的指针所指向的具体类型。
• FieldByName(name string) reflect.StructField 方法会返回一个 reflect.StructField 类型的值,该值描述了结构体的一个字段。参数 name 表示字段的名称,该方法会根据名称在结构体中查找并返回对应的字段。
• e.Field() 返回验证错误的字段名。
因此,getObj.Elem().FieldByName(e.Field()) 的作用是根据报错字段名,在结构体中查找并返回对应的字段的描述信息(reflect.StructField 类型)。
完整代码:
package main
import (
"reflect"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
type User struct {
Name string `binding:"required" msg:"请输入用户名"`
Age int `binding:"gte=18" msg:"未成年人禁止访问"`
Gender string `binding:"oneof=woman man" msg:"请从woman和man中选择输入"`
}
func custm(c *gin.Context) {
var user User
err := c.ShouldBindJSON(&user)
if err != nil {
c.JSON(200, gin.H{"msg": GetValidMsg(err, &user)})
// return
}
c.JSON(200, gin.H{"msg": user})
}
func GetValidMsg(err error, obj any) string {
// 使用的时候,需要传obj的指针
getObj := reflect.TypeOf(obj)
var tt string
// 将err接口断言为具体类型
if errs, ok := err.(validator.ValidationErrors); ok {
// 断言成功
for _, e := range errs {
// 循环每一个错误信息
// 根据报错字段名,获取结构体的具体字段
if f, exits := getObj.Elem().FieldByName(e.Field()); exits {
msg := f.Tag.Get("msg")
tt = tt + msg + " "
}
}
return tt
}
return err.Error()
}
func main() {
e := gin.Default()
e.GET("/custm", custm)
e.Run(":8010")
}
自定义验证器
此处自定义一个name不能在给定的标签中的绑定器
- 注册验证器函数,在初始化路由之后(main函数中)
// github.com/go-playground/validator/v10
// 注意这个版本得是v10的
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("sign", signValid)
}
这里得到绑定器名为sign,结构体中写在binding中
type User struct {
Name string `binding:"required,sign" msg:"请输入用户名"`
Age int `binding:"gte=18" msg:"未成年人禁止访问"`
Gender string `binding:"oneof=woman man" msg:"请从woman和man中选择输入"`
}
2.编写函数
func signValid(fl validator.FieldLevel) bool {
var nameList []string = []string{"fengfeng", "wang", "zhang3"}
name := fl.Field().Interface().(string)
for _, v := range nameList {
if name == v {
return false
}
}
return true
}