bigywater.tech
Gin · 34

绑定 time.Time、切片、嵌套结构

查询串里的日期怎么写、时区跟谁走、逗号分隔的数组为什么绑不上、?n= 空值是不是报错?这篇把 time.Time、切片和常见类型转换在 form 绑定里的行为一项项实测,并看自定义类型怎么接管解析。

-04:00 / Z / +08:00
不写时区 / time_utc:"1" / time_location 三种写法绑出的时区(进程时区 America/New_York)
1,2,3 → 报错
没写 collection_format 时,?multi=1,2,3 被当成一个整数解析
空值 → 0
?n=、?b=、?f=、?d= 都不报错,当作零值

time.Time 与时区

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

分步演示

时间、切片与类型转换
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。时间实验用环境变量 TZ=America/New_York(第二节 TZ=UTC)切换 time.Local;请求用 httptest 直接调用 ServeHTTP;每种情形各一次
    • 没有验证:time_format 对 RFC1123 等其它布局的行为(只试了 RFC3339 / 2006-01-02 / unix);夏令时边界的时间解析;parser=encoding.TextUnmarshaler 选项(源码里有,没有跑);multipart 表单里的数组与时间(规则相同,没有单独验证);map 类型字段的绑定
    京ICP备2026067502号-1京公网安备11010502063487号