Golang中web参数校验的实现
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">参数校验</a></li><li><a href="#_label1">基本用法</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_0">JSON</a></li><li><a href="#_lab2_1_1">URL</a></li></ul><li><a href="#_label2">常用的校验规则</a></li><ul class="second_class_ul"></ul><li><a href="#_label3">自定义错误信息</a></li><ul class="second_class_ul"></ul><li><a href="#_label4">自定义校验规则</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>参数校验</h2><p>确保我们的应用接收到的数据是格式正确并且安全的</p>
<p class="maodian"><a name="_label1"></a></p><h2>基本用法</h2>
<p class="maodian"><a name="_lab2_1_0"></a></p><h3>JSON</h3>
<p>假设我们正在开发一个<strong>登录接口</strong>,希望用户必须提供用户名和密码。在 Gin 中,我们首先会定义一个 Go 的 <strong>struct</strong> 来描述这个数据结构。</p>
<div class="jb51code"><pre class="brush:go;">type Login struct {
User string `json:"user" binding:"required"`
Password string `json:"password" binding:"required"`
}
</pre></div>
<ul><li>json:“…” 标签: 这个标签告诉 Gin,当它解析传入的 JSON 数据时,JSON 中的 user 字段应该映射到我们结构体中的 User 字段。</li><li>binding:“required” 标签: 这是最核心的部分!binding 就是用来做数据校验的。required 是一个校验规则,意思是“这个字段是必填的,不能为空”。</li></ul>
<div class="jb51code"><pre class="brush:go;">func loginHandler(c *gin.Context) {
var login Login
// 尝试将请求的 JSON body 绑定到 login 结构体上
// 在绑定的同时,Gin 会根据 "binding" 标签进行校验
err := c.ShouldBindJSON(&login)
if err != nil {
// 如果 err 不是 nil,说明校验失败了!
// 比如,某个 "required" 的字段没有被提供。
// Gin 通常会自动返回一个 400 Bad Request 错误。
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 如果 err 是 nil,说明所有校验都通过了!
c.JSON(200, gin.H{"status": "you are logged in"})
}
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202511/2025110209484150.png" /></p>
<p class="maodian"><a name="_lab2_1_1"></a></p><h3>URL</h3>
<p>除了处理 JSON,我们经常还需要处理来自 URL 查询(Query)和 HTML 表单(Form)的数据。<br /><strong>Gin 的设计非常统一。我们的 struct 定义方式几乎不变,只需要更换一个绑定方法就可以了。</strong></p>
<p><strong>如何校验 URL 查询参数,比如这样一个请求:GET /search?keyword=gin&page=1</strong><br />我们希望 keyword 是必填的,并且 page 必须是一个大于 0 的数字。<br />首先,还是定义我们的 struct:</p>
<div class="jb51code"><pre class="brush:go;">type SearchQuery struct {
Keyword string `form:"keyword" binding:"required"`
Page int `form:"page" binding:"required,gt=0"`
}
</pre></div>
<ul><li><strong>我们用了 form:“…” 标签</strong>,而不是 json:“…”。这个 form 标签既可以用于 URL 查询参数,也可以用于表单数据。</li><li>我在 Page 字段的 binding 里加了一个<strong>新规则 gt=0</strong>,它的意思是 “<strong>greater than 0</strong>”<strong>(必须大于0</strong>)。</li></ul>
<div class="jb51code"><pre class="brush:go;">func searchHandler(c *gin.Context) {
var query SearchQuery
// 从 URL query 中绑定并校验参数
// 注意这里换成了 ShouldBindQuery
err := c.ShouldBindQuery(&query)
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 校验通过!
c.JSON(200, gin.H{
"message": "search parameters are valid",
"keyword": query.Keyword,
"page": query.Page,
})
}
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202511/2025110209484173.png" /></p>
<p class="maodian"><a name="_label2"></a></p><h2>常用的校验规则</h2>
<p>我们通过一个用户注册的例子来看看:</p>
<div class="jb51code"><pre class="brush:go;">type RegisterUser struct {
// 用户名:必填,长度在 4 到 20 个字符之间
Username string `json:"username" binding:"required,min=4,max=20"`
// 邮箱:必填,且必须是合法的邮箱格式
Email string `json:"email" binding:"required,email"`
// 年龄:选填,但如果提供了,必须大于等于 18 岁,小于等于 100 岁
Age int `json:"age" binding:"gte=18,lte=100"`
// 用户类型:必填,并且值必须是 'user' 或 'admin' 中的一个
UserType string `json:"user_type" binding:"required,oneof=user admin"`
}
</pre></div>
<ul><li>min=4,max=20:用于字符串、数组等,限制其最小和最大长度。多个规则用逗号 , 分隔。</li><li>email:检查字符串是否符合标准的 email 格式。</li><li>gte=18:意思是 “Greater Than or Equal To”,即大于或等于 18。</li><li>lte=100:意思是 “Less Than or Equal To”,即小于或等于 100。</li><li>oneof=user admin:表示这个字段的值必须是后面列出的值之一(用空格分隔)。</li></ul>
<div class="jb51code"><pre class="brush:go;">func registerUserHandler(c *gin.Context) {
var register RegisterUser
err := c.ShouldBindJSON(&register)
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"username": register.Username,
"email": register.Email,
})
}
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202511/2025110209484153.png" /></p>
<p class="maodian"><a name="_label3"></a></p><h2>自定义错误信息</h2>
<p>核心思路是:<strong>捕获到校验错误后,我们不再直接把它整个返回,而是逐一检查是哪个字段的哪条规则出错了,然后根据这些信息,手动“翻译”成我们想展示的话。</strong></p>
<div class="jb51code"><pre class="brush:go;">func registerUserHandler(c *gin.Context) {
var register RegisterUser
err := c.ShouldBindJSON(&register)
if err != nil {
var verrs validator.ValidationErrors
ok := errors.As(err, &verrs)
if !ok {
// 如果不是 ValidationErrors 类型的错误,直接返回原始错误
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 创建一个 map 来存放我们“翻译”后的错误信息
// key 是字段名,value 是错误信息
// 比如:{"Username": "长度不能小于 4 个字符"}
translatedErrors := make(mapstring)
for _, fe := range verrs {
// fe.Field() 获取出错的字段名,比如 "Username"
translatedErrors = getErrorMsg(fe)
}
c.JSON(400, gin.H{"errors": translatedErrors})
return
}
c.JSON(200, gin.H{
"username": register.Username,
"email": register.Email,
})
}
// 这个函数专门用来“翻译”错误信息
func getErrorMsg(fe validator.FieldError) string {
// fe.Tag() 能获取到是哪个校验规则出错了,比如 "required", "min", "email"
switch fe.Tag() {
case "required":
return "这是必填项"
case "min":
// fe.Param() 能获取到规则后面的参数,比如 "min=4" 中的 "4"
return "长度不能小于 " + fe.Param() + " 个字符"
case "max":
return "长度不能超过 " + fe.Param() + " 个字符"
case "email":
return "请输入正确的邮箱地址"
case "oneof":
return "必须是 " + fe.Param() + " 中的一个"
default:
return "未知错误"
}
}
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202511/2025110209484115.png" /></p>
<p class="maodian"><a name="_label4"></a></p><h2>自定义校验规则</h2>
<p>当内置的规则(像 required, email, min, max 等)不够用时,我们就需要自定义规则。比如,我们想强制要求“密码必须同时包含字母和数字”。</p>
<p><strong>整个过程分为三步,我们一步一步来:</strong></p>
<ol><li>编写一个自定义校验函数<br />这个函数需要一个特定的格式。它接收一个 <strong>validator.FieldLevel</strong> 类型的参数,并返回一个 <strong>bool</strong> 值。返回 <strong>true 表示校验通过,false 表示失败。</strong></li></ol>
<div class="jb51code"><pre class="brush:go;">// isPasswordStrong 就是我们的自定义校验函数
func isPasswordStrong(fl validator.FieldLevel) bool {
// fl.Field().String() 可以获取到字段的字符串值
password := fl.Field().String()
hasLetter := false
hasDigit := false
// 遍历密码字符串,检查是否同时包含字母和数字
for _, char := range password {
if unicode.IsLetter(char) {
hasLetter = true
}
if unicode.IsDigit(char) {
hasDigit = true
}
}
return hasLetter && hasDigit
}
</pre></div>
<ol start="2"><li><p>将自定义函数“注册”到 Gin 的校验器里<br />光写好函数还不行,我们得告诉 Gin 的校验器:“嘿,我这里有一个新的校验规则,它的名字叫 strong_password,对应的处理函数是 isPasswordStrong。”</p>
<p>这个注册过程通常在你的程序启动时(比如 main 函数里)完成。</p></li></ol>
<div class="jb51code"><pre class="brush:go;">// 从 Gin 的 binding 中获取底层的 validator 引擎
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册我们的自定义校验函数
// 第一个参数是这个规则在 struct tag 中使用的名字
// 第二个参数是我们的函数
v.RegisterValidation("strong_password", isPasswordStrong)
}
</pre></div>
<ol start="3"><li>在 Struct Tag 中使用新规则<br />一旦注册成功,我们就可以像使用任何内置规则一样,在 struct tag 中使用 strong_password 了。</li></ol>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202511/2025110209484118.png" /></p>
頁:
[1]