查询串里的日期怎么写、时区跟谁走、逗号分隔的数组为什么绑不上、?n= 空值是不是报错?这篇把 time.Time、切片和常见类型转换在 form 绑定里的行为一项项实测,并看自定义类型怎么接管解析。
time.Local = America/New_York ;请求 ?def=2024-05-01T10:00:00Z&day=2024-05-01&dayu=2024-05-01&days=2024-05-01&unix=1714557600&unixm=1714557600123
err=<nil>
Def 2024-05-01T10:00:00Z (Location=UTC)
Day 2024-05-01T00:00:00-04:00 (Location=America/New_York)
DayU 2024-05-01T00:00:00Z (Location=UTC)
DayS 2024-05-01T00:00:00+08:00 (Location=Asia/Shanghai)
Unix 2024-05-01T06:00:00-04:00 (Location=America/New_York)
UnixM 2024-05-01T06:00:00-04:00 (Location=America/New_York)form 绑定里的 time.Time 默认按 RFC3339,字符串里自带偏移就以它为准。自定义 time_format 又没有时区信息时,默认用 time.Local——同一份代码在不同机器上解出不同的时刻,所以要显式写 time_utc:"1" 或 time_location。unix、unixmilli 等格式把整数当时间戳,结果是 Local 时区表示的时间。
/x?t=2024-05-01 → T=true err=parsing time "2024-05-01" as "2006-01-02T15:04:05Z07:00": cannot parse "" as "T"
/x?t= → T=true err=<nil>
/x?t=2024-05-01T10:00:00%2B08:00 → T=false err=<nil>
/x?t=2024-05-01+10:00:00 → T=true err=parsing time "2024-05-01 10:00:00" as "2006-01-02T15:04:05Z07:00": cannot parse " 10:00:00" as "T"
/x?d=2024-02-30 → T=true err=parsing time "2024-02-30": day out of range
/x?d=2024/05/01 → T=true err=parsing time "2024/05/01" as "2006-01-02": cannot parse "/05/01" as "-"
JSON {"t":"2024-05-01T10:00:00Z"} → 2024-05-01T10:00:00Z err=<nil>
JSON {"t":"2024-05-01"} → 0001-01-01T00:00:00Z err=parsing time "2024-05-01" as "2006-01-02T15:04:05Z07:00": cannot parse "" as "T"
JSON {"t":1714557600} → 0001-01-01T00:00:00Z err=json: cannot unmarshal number into Go struct field J.t of type time.Time
JSON {"t":null} → 0001-01-01T00:00:00Z err=<nil>格式不符报 parsing time … cannot parse …;空值不报错,保持零时间(配合 required 才会失败)。URL 里没编码的 + 在查询串里是空格,所以带 +08:00 的 RFC3339 时间要把加号写成 %2B。JSON 里的 time.Time 只认 RFC3339 字符串,数字不行。
/x?multi=1&multi=2&multi=3 → {"Multi":[1,2,3],"Csv":null,"Ssv":null,"Pipes":null,"Def":[7,8,9],"Arr":[0,0,0],"Strs":null} err=<nil>
/x?multi=1,2,3 → {"Multi":null,"Csv":null,"Ssv":null,"Pipes":null,"Def":null,"Arr":[0,0,0],"Strs":null} err=strconv.ParseInt: parsing "1,2,3": inval
/x?csv=1,2,3 → {"Multi":null,"Csv":[1,2,3],"Ssv":null,"Pipes":null,"Def":[7,8,9],"Arr":[0,0,0],"Strs":null} err=<nil>
/x?csv=1&csv=2,3 → {"Multi":null,"Csv":[1,2,3],"Ssv":null,"Pipes":null,"Def":[7,8,9],"Arr":[0,0,0],"Strs":null} err=<nil>
/x?ssv=a+b+c → {"Multi":null,"Csv":null,"Ssv":["a","b","c"],"Pipes":null,"Def":[7,8,9],"Arr":[0,0,0],"Strs":null} err=<nil>
/x?pipes=a|b|c → {"Multi":null,"Csv":null,"Ssv":null,"Pipes":["a","b","c"],"Def":[7,8,9],"Arr":[0,0,0],"Strs":null} err=<nil>
/x → {"Multi":null,"Csv":null,"Ssv":null,"Pipes":null,"Def":[7,8,9],"Arr":[0,0,0],"Strs":null} err=<nil>
/x?def=1 → {"Multi":null,"Csv":null,"Ssv":null,"Pipes":null,"Def":[1],"Arr":[0,0,0],"Strs":null} err=<nil>
/x?arr=1,2,3 → {"Multi":null,"Csv":null,"Ssv":null,"Pipes":null,"Def":[7,8,9],"Arr":[1,2,3],"Strs":null} err=<nil>
/x?arr=1,2 → {"Multi":null,"Csv":null,"Ssv":null,"Pipes":null,"Def":[7,8,9],"Arr":[0,0,0],"Strs":null} err=["1" "2"] is not valid value for [3]i
/x?multi= → {"Multi":[0],"Csv":null,"Ssv":null,"Pipes":null,"Def":[7,8,9],"Arr":[0,0,0],"Strs":null} err=<nil>
/x?strs=&strs= → {"Multi":null,"Csv":null,"Ssv":null,"Pipes":null,"Def":[7,8,9],"Arr":[0,0,0],"Strs":["",""]} err=<nil>
/x?strs=a,b → {"Multi":null,"Csv":null,"Ssv":null,"Pipes":null,"Def":[7,8,9],"Arr":[0,0,0],"Strs":["a,b"]} err=<nil>重复的键是 form 里切片的“原生”写法。逗号、空格、竖线分隔必须用 collection_format 声明(csv / ssv / tsv / pipes),否则整个字符串当一个元素(整数切片会因此报错)。default=7;8;9 用分号分隔默认值。固定长度的数组必须正好给够元素。空值对整数切片会得到 [0],要小心。
/x?n=12&u=200&b=true&f=1.5&d=1m30s → {N:12 U:200 B:true F:1.5 D:1m30s} err=<nil>
/x?n=abc → {N:0 U:0 B:false F:0 D:0s} err=strconv.ParseInt: parsing "abc": invalid syntax
/x?n=99999999999999999999 → {N:0 U:0 B:false F:0 D:0s} err=strconv.ParseInt: parsing "99999999999999999999": value out of range
/x?u=256 → {N:0 U:0 B:false F:0 D:0s} err=strconv.ParseUint: parsing "256": value out of range
/x?u=-1 → {N:0 U:0 B:false F:0 D:0s} err=strconv.ParseUint: parsing "-1": invalid syntax
/x?b=maybe → {N:0 U:0 B:false F:0 D:0s} err=strconv.ParseBool: parsing "maybe": invalid syntax
/x?b=1&f=1e3 → {N:0 U:0 B:true F:1000 D:0s} err=<nil>
/x?d=5+minutes → {N:0 U:0 B:false F:0 D:0s} err=time: unknown unit " minutes" in duration "5 minutes"
/x?n=&b=&f=&d= → {N:0 U:0 B:false F:0 D:0s} err=<nil>
/x?n=%2012%20 → {N:12 U:0 B:false F:0 D:0s} err=<nil>错误信息都是 strconv 的原文,直接返回给用户并不友好,通常要自己翻译。空值一律当零值,不报错;非字符串类型会先去掉前后空格。
/x?lv=high → {Lv:3 Lvs:[] Items:[]} err=<nil>
/x?lv=ultra → {Lv:0 Lvs:[] Items:[]} err=未知等级 "ultra"
/x?lvs=low&lvs=high → {Lv:0 Lvs:[1 3] Items:[]} err=<nil>
/x?items={"sku":"a","n":1}&items={"sku":"b","n":2} → {Lv:0 Lvs:[] Items:[{Sku:a N:1} {Sku:b N:2}]} err=<nil>
/x?items=oops → {Lv:0 Lvs:[] Items:[]} err=invalid character 'o' looking for beginning of value字段类型实现 UnmarshalParam(string) error 就可以接管解析(BindUnmarshaler),适合枚举、ID 之类;返回的错误原样成为绑定的错误。结构体切片则是每个参数值按 JSON 解码。