
字节笔记本
2026年10月7日 · 约 11 分钟读完
带引号的数字绑不进 int
前端发出去的 HTTP 正文是一段文本,JSON 里面的值仍然有类型。数字、字符串、布尔是三种值。Gin 的 ShouldBindJSON 走 encoding/json,JSON 数字可以进 int,带引号的 "25" 是字符串,进 int 会失败。

ShouldBindJSON 把错误交回来
对话里的结构体是一份用户入参。Name 带 binding:"required",Age 是 int,规则是 required,gte=0,lte=130,Email 要过 email,Password 至少 min=6。标签 json 决定字段名,标签 binding 决定校验。两边都要写,只写 json 不会校验范围。
处理函数里声明 var user User,调用 c.ShouldBindJSON(&user)。返回的 err 由自己决定怎么回给客户端。样本是 400,正文里放 err.Error()。BindJSON 不一样:出错时它会自己 AbortWithError,后面的代码不应该再假设还能改状态码。要自己组错误 JSON,用 ShouldBindJSON。
校验通过之后,样本只把 name、age、email 放进响应。password 留在服务端。不要把绑定成功的整个结构体原样 c.JSON 回去,否则密码会进访问日志和浏览器。
对应的请求是:
curl -X POST http://localhost:8080/user \
-H "Content-Type: application/json" \
-d '{"name":"张三","age":25,"email":"zhangsan@example.com","password":"123456"}'这里的 25 没有引号,它是 JSON 数字。123456 作为示例密码只说明长度能过 min=6,上线不要用这组值。
带引号的数字进不了 int
有人会觉得表单和输入框出来的都是字符串,所以 JSON 里的年龄也会是 "25"。文本框里的字符是字符串,放进 JSON 时要由发送方决定写成数字还是字符串。JSON.stringify 对 JavaScript 数字会写成不带引号的数字。若代码写成 age: String(25) 或输入框的值没有 Number(),载荷里就是 "age":"25"。
encoding/json 不会把 JSON 字符串转成 int、float64 或 bool。"25" 解到 int,"1.75" 解到 float64,"true" 解到 bool,得到的是类型错误。数字 25、浮点 1.75、布尔 true 才能对上字段类型。对话后半段那份“即使带引号也能自动转”的示例,和这个解析器的行为不一致,按那个示例去发请求,ShouldBindJSON 会在校验之前就失败。
时间字段更严。样本里的 JoinDate 是 time.Time,对应的文本是 2024-01-01T00:00:00Z 这种 RFC3339。写成 2024-01-01 对不上。Birthday 在样本里是 string,那就可以是任意日期文本,解析器不会帮你变成时间。数组 hobbies 对 []string,嵌套的 address 对匿名结构体,键用 city 和 country。缺一层对象,整次绑定失败,不会只留下一半字段。
错误要分成语法和类型
绑定失败时,样本用类型开关区分 *json.UnmarshalTypeError 和 *json.SyntaxError。前者是值的类型和字段对不上,比如年龄收到了字符串或 "abc"。后者是正文根本不是 JSON,比如少了逗号或括号。校验失败(年龄 200 超过 lte=130、邮箱不像邮箱)是另一类错误,来自 binding,不是这两种。
三种失败都应该回 400,正文里不要把内部堆栈倒给调用方。日志里可以留下字段名。UnmarshalTypeError 上能看到是哪个字段。调用方如果坚持用字符串传数字,就在结构体里把该字段改成 string,再用 strconv.Atoi 转,失败时返回明确的字段错误。不要依赖解析器偷偷转换。
空正文、Content-Type 不是 JSON,也会走绑定失败。客户端要带 Content-Type: application/json。curl 示例里这一头不能省。

把每个字段按 JSON 类型走一遍
第二份结构体 UserInfo 把类型摆全了。Name 是 string,JSON 里用双引号。Age 是 int,Height 是 float64,两者都要 JSON 数字。IsStudent 是 bool,JSON 里写 true 或 false,不写 "true"。Birthday 声明成 string,所以 "1998-01-01" 能留下。JoinDate 是 time.Time,样本值是 "2024-01-01T00:00:00Z"。Hobbies 是 []string,元素各自带引号。Address 是嵌套结构,city 和 country 都是字符串。
绑定成功后,样本用 fmt.Printf 打 Name、Age、Height、IsStudent 的 %T 和 %v,用来确认类型,而不是用来把响应拼出来。响应里的 data 是整份 userInfo。这份结构没有密码字段。前一个接口有密码,响应用单独的 gin.H 只挑三个字段。两个接口不要抄混。
Age 上可以再加 binding:"required,gte=0,lte=150"。注意两份样本的上限不一样,一份是 130,一份是 150。同一个字段在两个接口里要选一个上限,写进文档,不要让调用方猜。required 看的是有没有值。JSON 里出现 "age":0 时,int 的零值和“没传”在绑定层不好分,范围检查 gte=0 又允许零。年龄若不允许缺省,调用方应始终带上这个键,服务端用指针或单独的存在性判断,而不是只靠 required 对 int 的默认行为。
失败信息分三支
ShouldBindJSON 的 err 先看是不是 *json.SyntaxError。正文缺括号、多一个逗号、根本不是对象,走这里。再看是不是 *json.UnmarshalTypeError。年龄是 "abc" 或 "25",身高是 "1.75" 这种带引号的文本,布尔是 "true",都落在类型错误。两种都 return,不要继续用零值的 userInfo 去写库。零值年龄是 0,会穿过 gte=0,变成一条看起来合法的数据。
类型已经对上、规则没过,例如年龄 200、密码短于 6、邮箱没有 @,错误来自 binding 标签:required、email、min=6、gte 和 lte。样本把 err.Error() 放进 400。这串文字是给开发看的,可以回给自己的前端,不要假设它稳定到能当接口合同。合同用固定的错误码,日志里留原始 err。
curl 必须带 Content-Type: application/json。漏了这个头,Gin 可能不按 JSON 绑定。正文里的 age 保持数字。密码字段只出现在请求里。
数字不带引号再校验
ShouldBindJSON 把错误交回处理函数,BindJSON 会自己中止。JSON 数字进 int,带引号的数字是字符串,会得到 UnmarshalTypeError。范围、邮箱、最短长度靠 binding 标签,发生在类型已经对上之后。响应里不要回写密码。时间用 RFC3339 对 time.Time,普通日期文本就声明成 string。
上线前用两组 curl 把类型钉死。第一组里 age 是数字 25,height 是数字,is_student 是布尔 true,join_date 是 2024-01-01T00:00:00Z,期望 200。第二组把年龄改成 "25",期望 400,并且日志里是类型错误,库里没有新行。密码只出现在第一组的请求里,响应 JSON 里搜不到这个字段。邮箱用样本里的 zhangsan@example.com 这种格式过 email 标签,再准备一个没有 @ 的值确认校验会拒。
gte 和 lte 的上限在两份样本里分别是 130 和 150。对外只公布一个。min=6 只约束长度,不约束强度。示例里的 123456 能通过校验,不能当作可以接受的口令。绑定成功只说明形状对了,账号是否创建是后面的逻辑。BindJSON 会在错误时自己中止,和 ShouldBindJSON 不要写在同一个处理函数里各调用一次,否则错误路径会走两次。
结构体标签写成两排。json 对上键名,binding 写 required、email、min=6、gte=0 和 lte。只写 json 时,类型对了就会进业务,范围不会被拦。只写 binding 时,键名对不上,字段一直是零值,校验可能因为零值而失败,看起来像客户端没传。两排一起写,再拿数字和带引号的数字各打一次,才能确定失败发生在解析而不是发生在规则。
Hobbies 这种数组,元素必须已经是 JSON 字符串,不能把整个数组再包成一个字符串。嵌套的 address 必须是对象。少了这层,绑定失败,不会留下只有城市没有国家的半份结构。处理函数在 err != nil 时返回,避免用零值继续 Create。零值能穿过一部分数值范围,这是类型错误被忽略之后才会出现的脏数据。响应里不回密码,日志里可以留绑定错误的字段名,调用方只看固定错误码。



