bigywater.tech
Gin · 30

绑定 tag:form、json、uri、header、query

form、json、uri、header、query 这些 tag 到底谁读谁?没写 tag 时用什么名字?ShouldBind 是怎么决定该用哪个绑定器的?这篇用同一个结构体、四个不同的 tag 名,把每种来源读到的值一个个对出来,再看几个容易踩的边界。

1 / 3 / 5 / 11
ShouldBindQuery / Uri / Header / JSON 对同一结构体分别读到的值(四个 tag 名不同)
区分大小写
没有 tag 时 key=字段名:?Name 能绑,?name 不能
GET 忽略 body
GET + JSON body 经 ShouldBind 绑出 0,没有错误

各读各的 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 匹配,不区分大小写;表单和查询串的键区分大小写。

没有 tag 的字段

实测 · 本机执行输出
    /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" 这样的选项可以给缺省值。

ShouldBind 怎么选

实测 · 本机执行输出
    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           form

binding.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 解码到这个结构体里,而不是展开它的字段。

分步演示

绑定 tag
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。请求用 httptest 直接调用 ServeHTTP;表格那一节直接调用 binding.Default 这个纯函数;每种情形各一次
    • 没有验证:multipart 表单与文件字段的 tag(后面上传的文章会涉及);XML、YAML、TOML 的 tag 与大小写规则;自定义类型实现 BindUnmarshaler 的行为(源码里有,没有跑);parser=encoding.TextUnmarshaler 选项;header 标签里带下划线的头名的规范化
    京ICP备2026067502号-1京公网安备11010502063487号