bigywater.tech
Gin · 36

多余字段与缺失字段的行为

前端少传了字段、多传了字段、传了 null,绑定之后结构体里到底是什么?预填默认值能不能用?绑定出错时部分字段是不是已经被赋值?这篇把这些边界一个个试出来,最后给 PATCH 场景做个能区分没传、null、零值的类型。

保持预填值
缺失字段不改动:JSON {} → Page:1 Size:20
map 合并
预填的 map 与请求里的 m 合并,旧键保留,而切片被整体替换
3 种状态
Opt[T] 区分没传 / null / 零值,普通的 *T 只能区分两种

缺失与零值

实测 · 本机执行输出
    预填 Page=1 Size=20 Sort=id Tag=预填标签
    JSON {}                                                   → {Page:1 Size:20 Sort:"id" Tag:预填标签} err=<nil>
    JSON {"page":3}                                           → {Page:3 Size:20 Sort:"id" Tag:预填标签} err=<nil>
    JSON {"page":0,"sort":""}                                 → {Page:0 Size:20 Sort:"" Tag:预填标签} err=<nil>
    JSON {"page":null,"size":null,"sort":null,"tag":null}     → {Page:1 Size:20 Sort:"id" Tag:nil} err=<nil>
    JSON {"unknown":1}                                        → {Page:1 Size:20 Sort:"id" Tag:预填标签} err=<nil>
    查询 /x                                                  → {Page:1 Size:20 Sort:"id" Tag:预填标签} err=<nil>
    查询 /x?page=3                                           → {Page:3 Size:20 Sort:"id" Tag:预填标签} err=<nil>
    查询 /x?page=0&sort=                                     → {Page:0 Size:20 Sort:"" Tag:预填标签} err=<nil>
    查询 /x?page=&size=                                      → {Page:0 Size:0 Sort:"id" Tag:预填标签} err=<nil>

缺失的字段不会被碰,所以可以预先把默认值填进结构体再绑定(JSON 与查询串都是这样)。传了零值就是覆盖成零值:{"page":0} 会把预填的 Page=1 改成 0,而不是保留默认值。null 对非指针字段是空操作,对指针则置为 nil。

预填的复合值

实测 · 本机执行输出
    预填 M={keep:1 over:2} S=[9 9 9 9] Arr=[7 7 7] In={5 6} P=&{100 200}
    JSON {}
      → M=map[keep:1 over:2](nil:false) S=[9 9 9 9](nil:false) Arr=[7 7 7] In={A:5 B:6} P={A:100 B:200} 同一个 *Sub 被改写:true err=<nil>
    JSON {"m":{"over":22,"new":3},"s":[1,2],"arr":[1],"in":{"a":50},"p":{"b":999}}
      → M=map[keep:1 new:3 over:22](nil:false) S=[1 2](nil:false) Arr=[1 0 0] In={A:50 B:6} P={A:100 B:999} 同一个 *Sub 被改写:true err=<nil>
    JSON {"m":null,"s":null,"p":null}
      → M=map[](nil:true) S=[](nil:true) Arr=[7 7 7] In={A:5 B:6} P=nil 同一个 *Sub 被改写:false err=<nil>

预填的 map 会与请求里的对象合并(旧键保留),切片整体替换,较短的数组把多出的位置清零,嵌套结构体逐字段合并,非 nil 的指针会被原地改写——如果预填的指针指向共享对象,一个请求会永久改掉它。绑定用的结构体每次都应该新建,不要预填共享的指针或 map。

出错之后的状态

实测 · 本机执行输出
    {"a":"x","b":"ok","c":3,"d":[1,2]}             → {A:0 B:ok C:3 D:[1 2]} err=json: cannot unmarshal string into Go struct field Req.a of type int
    {"a":1,"b":2,"c":3}                            → {A:1 B: C:3 D:[]} err=json: cannot unmarshal number into Go struct field Req.b of type string
    {"a":1,"b":"ok","c":"z","d":["q"]}             → {A:1 B:ok C:0 D:[0]} err=json: cannot unmarshal string into Go struct field Req.c of type int
    {"a":1,"b":"ok"; "c":3}                        → {A:0 B: C:0 D:[]} err=invalid character ';' after object key:value pair
    {"a":1,"b":"ok","c":3                          → {A:0 B: C:0 D:[]} err=unexpected EOF

JSON 类型错误不会中止解码,所以出错时结构体可能处在“一部分字段已赋值”的状态;语法错误则整体不赋值。绑定返回错误之后应当直接返回 400,不要使用结构体里的任何值。

三态字段

实测 · 本机执行输出
    {}
      → Ptr=nil Raw="" Age=没传 Name=没传 err=<nil>
    {"ptr":null,"raw":null,"age":null,"name":null}
      → Ptr=nil Raw="null" Age=null Name=null err=<nil>
    {"ptr":0,"raw":0,"age":0,"name":""}
      → Ptr=&0 Raw="0" Age=值=0 Name=值= err=<nil>
    {"ptr":5,"raw":{"x":1},"age":30,"name":"ann"}
      → Ptr=&5 Raw="{\"x\":1}" Age=值=30 Name=值=ann err=<nil>

PATCH 接口要区分“没传(保持原值)”“传了 null(清空)”“传了零值”。指针字段分不开前两种;json.RawMessage 能分开但要二次解析;给泛型类型加一个 UnmarshalJSON 最直接:没传时这个方法根本不会被调用,所以 Set 保持 false。

分步演示

缺失与多余字段
本步的对象

    本文用到的工具与边界

    • 本机 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;预填值在 handler 里创建结构体时设置;Opt[T] 是实验里自己写的泛型类型;每种情形各一次
    • 没有验证:XML、YAML 绑定下缺失与 null 的行为;自定义类型实现 UnmarshalJSON 时与 binding 校验标签的交互;表单绑定里切片与 map 的预填合并规则(只验证了标量);encoding/json 在不同 Go 版本里对数组、null 的细节差异
    京ICP备2026067502号-1京公网安备11010502063487号