form、json、uri、header、query 这些 tag 到底谁读谁?没写 tag 时用什么名字?ShouldBind 是怎么决定该用哪个绑定器的?这篇用同一个结构体、四个不同的 tag 名,把每种来源读到的值一个个对出来,再看几个容易踩的边界。
ShouldBindQuery → {"j_n":1} err=<nil> | ShouldBindUri → {"j_n":3} err=<nil> | ShouldBindHeader → {"j_n":5} err=<nil>
ShouldBindJSON → {"j_n":11} err=<nil>
ShouldBindJSON → {"j_n":15} err=<nil> ← JSON 键不区分大小写(encoding/json)
ShouldBind(表单) → {"j_n":21} err=<nil>
ShouldBind(表单) → {"j_n":0} err=<nil> ← 表单键区分大小写ShouldBindJSON 读 json 标签,ShouldBindUri 读 uri,ShouldBindHeader 读 header,而 ShouldBindQuery 和表单绑定读的都是 form——Gin 没有 query 标签。同一个结构体想同时支持几种来源,就把几个 tag 都写上。JSON 键由 encoding/json 匹配,不区分大小写;表单和查询串的键区分大小写。
/x?Name=Ann&age=30 → {"Name":"Ann","Age":30,"Secret":"","Page":1} err=<nil> hidden=""
/x?name=Ann&age=30 → {"Name":"","Age":30,"Secret":"","Page":1} err=<nil> hidden=""
/x?NAME=Ann → {"Name":"","Age":0,"Secret":"","Page":1} err=<nil> hidden=""
/x?Secret=s&hidden=h&age=1 → {"Name":"","Age":1,"Secret":"","Page":1} err=<nil> hidden=""
/x?Age=40&age=41 → {"Name":"","Age":41,"Secret":"","Page":1} err=<nil> hidden=""不写 tag 时 key 取字段名本身(Name 就是 Name,不会转小写),所以 ?name= 绑不上 Name。form:"-" 与未导出字段不参与绑定。form:"page,default=1" 这样的选项可以给缺省值。
Content-Type \ 方法 GET POST PUT DELETE
"" form form form form
"application/json" form json json json
"application/xml" form xml xml xml
"text/xml" form xml xml xml
"application/x-www-form-urlencoded" form form form form
"multipart/form-data" form multipart/form-data multipart/form-data multipart/form-data
"application/yaml" form yaml yaml yaml
"application/toml" form toml toml toml
"application/x-protobuf" form protobuf protobuf protobuf
"text/plain" form form form formbinding.Default(method, contentType) 是个纯函数:GET 总是 form;其它方法按 Content-Type 选,不认识的类型和缺失的类型都退回 form。注意比较的是去掉参数之后的类型(application/json; charset=utf-8 仍是 json)。
GET + JSON body {n:1} : 绑定器=form → {"n":0} err=<nil>
GET + 查询串 ?n=2 : 绑定器=form → {"n":2} err=<nil>
POST + JSON body {n:3} : 绑定器=json → {"n":3} err=<nil>
POST + JSON body 但没有 Content-Type: 绑定器=form → {"n":0} err=<nil>
POST + JSON body,charset=utf-8 : 绑定器=json → {"n":5} err=<nil>
POST + 表单 n=6 + 查询串 ?n=7 : 绑定器=form → {"n":6} err=<nil>
POST + JSON body 且 URL 里有 ?n=8 : 绑定器=json → {"n":9} err=<nil>第一,GET 请求带 JSON body 时,ShouldBind 默默地用 form 绑定,body 被忽略,而且没有任何错误——字段全是零值。第二,POST 的 JSON 请求忘了设 Content-Type,同样退回 form。想要明确的行为就直接调用 ShouldBindJSON 之类的专用方法。
/x?id=1&x=2&level=3&p=4&sub=%7B%22Level%22%3A5%7D → {"ID":1,"Inner":{"X":2},"Opt":{"Level":3},"P":4,"Sub":{"Level":5}} Opt!=nil:true P!=nil:true err=<nil>
/x?sub=5 → {"ID":0,"Inner":{"X":0},"Opt":null,"P":null,"Sub":{"Level":0}} Opt!=nil:false P!=nil:false err=json: cannot unmarshal number into G
/x?id=1 → {"ID":1,"Inner":{"X":0},"Opt":null,"P":null,"Sub":{"Level":0}} Opt!=nil:false P!=nil:false err=<nil>
/x?level=3 → {"ID":0,"Inner":{"X":0},"Opt":{"Level":3},"P":null,"Sub":{"Level":3}} Opt!=nil:true P!=nil:false err=<nil>嵌入的、匿名的结构体字段直接展开;指针结构体只有请求里出现了它的字段才会被分配,所以 nil 可以表示“没传”。给结构体字段写了 form 标签时,Gin 把参数值整个当 JSON 解码到这个结构体里,而不是展开它的字段。