前端少传了字段、多传了字段、传了 null,绑定之后结构体里到底是什么?预填默认值能不能用?绑定出错时部分字段是不是已经被赋值?这篇把这些边界一个个试出来,最后给 PATCH 场景做个能区分没传、null、零值的类型。
预填 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 EOFJSON 类型错误不会中止解码,所以出错时结构体可能处在“一部分字段已赋值”的状态;语法错误则整体不赋值。绑定返回错误之后应当直接返回 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。