日期函数获取上一个月 bug, 在一些特殊日子才出现,比如今天 10-31 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
Qjues
V2EX    Go 编程语言

日期函数获取上一个月 bug, 在一些特殊日子才出现,比如今天 10-31

  •  1
     
  •   Qjues 4 天前 3413 次点击

    今天生产环境出现比较多的异常,挺纳闷的,毕竟最近该服务没有怎么迭代。

    经过排查定位发现,居然是某些特殊日子才会出现的问题。

    RetentiOnMonths= 3 MOnthTablePrefix= "jobs_"

    代码是一个分表逻辑中,获取最近月份的表,通过 AddDate(0, -i, 0) 函数获取前几个月的表。应返回当前月和前 3 个月的分表名数组。[jobs_202510 jobs_202509 jobs_202508 jobs_202507]( golang 1.19 )

    // GetRecentHotTables 获取最近 n 个月的热表名称列表 func (j jobPartitionRepositoryImpl) GetRecentHotTables() []string { months := RetentionMonths tables := make([]string, 0, months) now := time.Now() for i := 0; i <= months; i++ { date := now.AddDate(0, -i, 0) year := date.Format("2006") month := date.Format("01") tableName := fmt.Sprintf("%s%s%s", MonthTablePrefix, year, month) tables = append(tables, tableName) } return tables } 

    光看代码感觉是没啥问题的,为啥怀疑到这段代码,也是通过日志发现,查询表的 sql 只查 2025-10, 2025-08, 2025-07 表,缺了 2025-09 表。

    所以本地验证一番。(单元测试还通过了。。。因为验证的代码和里面一样。。。)

    === RUN TestGetRecentHotTables --- PASS: TestGetRecentHotTables (0.00s) === RUN TestGetRecentHotTables/默认获取最近 3 个月表 [jobs_202510 jobs_202510 jobs_202508 jobs_202507] --- PASS: TestGetRecentHotTables/默认获取最近 3 个月表 (0.00s) PASS 

    看输出发现 jobs_202510 jobs_202510 有两个,问题就在这里了。

    now := time.Now() fmt.Println(now.AddDate(0, -1, 0).Format("200601")) 

    预期是输出 202509 的,但现实是 202510

    那为什么呢?通过询问 google ,找到下面相关文章说明,大致意思就是,因为 10-31 减去一个月为 09-31, 但是又因为 9 月没有 31 号,就将日期标准化为 10-01 ,保证了 time 值的有效性。

    https://github.com/golang/go/issues/31145 https://learnku.com/articles/71760

    在 learnku 文章中提到 php ,然后用 php 类似函数试了一下,也有类似问题( php8.1 )

    echo date('Y-m-d', strtotime('-1 months')); // 2025-10-01 

    周五给我整个这个,真是够了。

    修改方案, 减去当前时间的天数,时间调整到上一个月最后一天:

    now := time.Now() now.AddDate(0, 0, -now.Day()) 

    js moment 试了是正常的

    console.log(moment(new Date()).subtract(1, 'months').format('YYYY-MM-DD')) 
    33 条回复    2025-11-04 13:50:37 +08:00
    Goooooos
        1
    Goooooos  
       4 天前 via Android   1
    每月 1 号再减
    body007
        2
    body007  
       4 天前
    学到新知识,就像我定每月最后一天的闹钟都是定每月 1 号提前一天提醒一样,这样可以消除每月天数不同的问题,所以可以按下面方式做吧。

    now := time.Now()
    now = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
    superjojo
        3
    superjojo  
       4 天前
    做过日期类软件,老多特殊情况了
    moefishtang
        4
    moefishtang  
       4 天前   1
    万圣节等于圣诞节是吧
    Oct(31)==Dec(25)
        5
    liaohongxing  
       4 天前 via Android
    我都是用 carbon 时间库处理
    liaohongxing
    lovelylain
        6
    lovelylain  
       4 天前 via Android   1
    当月 0 号就是上个月最后一天,了解这一点要方便很多,否则容易写出 bug 或不简洁代码。
    iseki
        7
    iseki  
       4 天前
    Go 这个 time.Time 确实不太好用,但是奈何标准库里只有这个。
    我检查了下,文档上写了:AddDate normalizes its result in the same way that Date does, so, for example, adding one month to October 31 yields December 1, the normalized form for November 31.
    咱写代码时理应能够意识到这个问题,意识到这个问题后理应知道去查阅文档。
    ZeroDu
        8
    ZeroDu  
       4 天前
    get 新知识,之前写 java ,里面就没这个问题
    iseki
        9
    iseki  
       4 天前
    我刚想甩锅给 Go 时间库做得差呢,结果一看,人家写了,你不看
    iseki
        10
    iseki  
       4 天前
    @ZeroDu Java 的时间库在这个地方行为和 Go 不太一样:For example, 2007-03-31 plus one month would result in the invalid date 2007-04-31. Instead of returning an invalid result, the last valid day of the month, 2007-04-30, is selected instead. 如果写代码时不注意,换一个需求一样可能踩坑
    joey9696
        11
    joey9696  
       4 天前
    时间边界 可以用 lancet 这个库
    mmdsun
        12
    mmdsun  
       4 天前 via iPhone
    Java 10-31 减去一个月为 09-31, 但是又因为 9 月没有 31 号,就会是 9 月最后一天。如果没记错的话。这样比较合理。
    liuliuliuliu
        14
    liuliuliuliu  
    PRO
       4 天前
    ```csharp
    var date = new DateTime(2025,10,31);
    var date2 = date.AddMonths(-1);

    Console.WriteLine(date2); // 2025-9-30 00:00:00

    ```
    cppc
        15
    cppc  
       4 天前
    func (t Time) AddDate(years int, months int, days int) Time

    这个 API 的形态就很烧脑,因为月份和日期都会有一个逻辑边界。应该像别的语言那样,调整年月日设计成三个独立的方法。
    pike0002
        16
    pike0002  
       3 天前
    我记得我们当时针对这个自己封装了个 AddDate()方法
    sunwq
        17
    sunwq  
       3 天前
    PHP 也出现了类似的问题
    rming
        18
    rming  
       3 天前 via iPhone


    python 的 relativedelta 应该没问题(代码是 copilot 写的)
    mcfog
        19
    mcfog  
       3 天前   1
    歪题:PHP 有丧心病狂的 last day previous month
    https://3v4l.org/eN9Ip
    ksedz
        20
    ksedz  
       3 天前
    10.31 -1 month 到 10.1 在语义上就是不对的.
    与其说 9.31 不存在时的处理问题,不如说是 -1 month 处理的太简陋了。
    EminemW
        21
    EminemW  
       3 天前
    哈哈哈,我之前也遇到这个问题,特地写了个 AddMonth 方法,防止后面的人踩坑
    AV1
        22
    AV1  
       3 天前
    做月份加减法本来就很奇怪,或者说没有一个很通用的计算方法,比如 3 月 31 日减一个月,是希望得到 2 月几日呢?
    archxm
        23
    archxm  
       2 天前 via Android
    @iseki 那程序员以后写代码前,得把每行注释过一遍了。
    iseki
        24
    iseki  
       2 天前 via Android
    @archxm 这个就看经验咯,前两天我也被某个自以为了解的 API 坑了一下,Go 里不是有个 exec/Command.Wait 嘛,我打死也没想到这个 API 会顺手把我 StdoutPipe 来的管子给关了,查了好一会儿才发现。
    bv
        25
    bv  
       1 天前
    这个问题在高版本中是不是已经修复了: https://go.dev/play/p/tQQi_o4N0YY
    Qjues
        26
    Qjues  
    OP
       1 天前
    @bv 日期设置成 10 月 31 号,time.Date(2025, time.October, 31, 0, 0, 0, 0, time.Local) 。并没有修复
    unco020511
        27
    unco020511  
       1 天前
    得用每月 1 号去减一天
    realJamespond
        28
    realJamespond  
       1 天前
    你这种不就是直接减 30 天的做法? 10 几年前 php 就遇到过
    littlefishzzz
        29
    littlefishzzz  
       1 天前
    @wenrouxiaozhu pendulum 库可以
    ```
    >>> import pendulum
    >>> dt = pendulum.datetime(2025, 10, 31)
    >>> dt.subtract(mOnths=1)
    DateTime(2025, 9, 30, 0, 0, 0, tzinfo=Timezone('UTC'))
    ```
    skallz
        30
    skallz  
       1 天前
    月份和年份本来就不能直接做加减,月份每月天数不一样,年份有闰年,一般系统的做法都是在产品说明里面写明一个月默认 30 天,一年默认 365 天,然后使用天数做加减的;其他加减月份和年份的库就算兼容了这些情况,但是实际表现的结果不统一,难以直观预测结果,反而会很奇怪,还不如提前告知用默认天数加减
    way2create
        31
    way2create  
       1 天前
    知道这个坑,感觉 PHP 那个文章已经说的很清楚了
    rekulas
        32
    rekulas  
       1 天前
    典型的日期 overflow 问题 一般用成熟的三方库比较好已经做了优化了,不然就得自己处理
    rrfeng
        33
    rrfeng  
       19 小时 13 分钟前
    先定义好减一个月是个什么运算
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     4594 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 22ms UTC 01:04 PVG 09:04 LAX 17:04 JFK 20:04
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86