The Go Programming Language
› http://golang.org/
› Go Playground
Go Projects
› Revel Web Framework
Qjues

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

  •  1
     
  •   Qjues · Oct 31, 2025 · 5764 views
    This topic created in 370 days ago, the information mentioned may be changed or developed.

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

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

    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'))
    
    37 replies  •  2025-11-12 10:48:56 +08:00
    Sezxy
    Sezxy
    Oct 31, 2025 via Android❤️1
    1
    每月 1 号再减
    body007
    body007
    Oct 31, 2025
    2
    学到新知识,就像我定每月最后一天的闹钟都是定每月 1 号提前一天提醒一样,这样可以消除每月天数不同的问题,所以可以按下面方式做吧。

    now := time.Now()
    now = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
    superjojo
    superjojo
    Oct 31, 2025
    3
    做过日期类软件,老多特殊情况了
    moefishtang
    moefishtang
    Oct 31, 2025❤️1
    4
    万圣节等于圣诞节是吧😂
    Oct(31)==Dec(25)
    liaohongxing
    liaohongxing
    Oct 31, 2025 via Android
    5
    我都是用 carbon 时间库处理
    lovelylain
    lovelylain
    Oct 31, 2025 via Android❤️1
    6
    当月 0 号就是上个月最后一天,了解这一点要方便很多,否则容易写出 bug 或不简洁代码。
    iseki
    iseki
    Oct 31, 2025
    7
    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
    ZeroDu
    Oct 31, 2025
    8
    get 新知识,之前写 java ,里面就没这个问题
    iseki
    iseki
    Oct 31, 2025
    9
    我刚想甩锅给 Go 时间库做得差呢,结果一看,人家写了,你不看······
    iseki
    iseki
    Oct 31, 2025
    10
    @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
    joey9696
    Oct 31, 2025
    11
    时间边界 可以用 lancet 这个库
    mmdsun
    mmdsun
    Oct 31, 2025 via iPhone
    12
    Java 10-31 减去一个月为 09-31, 但是又因为 9 月没有 31 号,就会是 9 月最后一天。如果没记错的话。这样比较合理。
    liuliuliuliu
    liuliuliuliu
    Oct 31, 2025
    14
    ```csharp
    var date = new DateTime(2025,10,31);
    var date2 = date.AddMonths(-1);

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

    ```
    cppc
    cppc
    Nov 1, 2025
    15
    func (t Time) AddDate(years int, months int, days int) Time

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


    python 的 relativedelta 应该没问题(代码是 copilot 写的)
    mcfog
    mcfog
    Nov 1, 2025❤️1
    19
    歪题:PHP 有丧心病狂的 last day previous month
    https://3v4l.org/eN9Ip
    ksedz
    ksedz
    Nov 1, 2025
    20
    10.31 -1 month 到 10.1 在语义上就是不对的.
    与其说 9.31 不存在时的处理问题,不如说是 -1 month 处理的太简陋了。
    EminemW
    EminemW
    Nov 1, 2025
    21
    哈哈哈,我之前也遇到这个问题,特地写了个 AddMonth 方法,防止后面的人踩坑
    AV1
    AV1
    Nov 1, 2025
    22
    做月份加减法本来就很奇怪,或者说没有一个很通用的计算方法,比如 3 月 31 日减一个月,是希望得到 2 月几日呢?
    archxm
    archxm
    Nov 2, 2025 via Android
    23
    @iseki 那程序员以后写代码前,得把每行注释过一遍了。
    iseki
    iseki
    Nov 2, 2025 via Android
    24
    @archxm 这个就看经验咯,前两天我也被某个自以为了解的 API 坑了一下,Go 里不是有个 exec/Command.Wait 嘛,我打死也没想到这个 API 会顺手把我 StdoutPipe 来的管子给关了,查了好一会儿才发现。
    bv
    bv
    Nov 3, 2025
    25
    这个问题在高版本中是不是已经修复了: https://go.dev/play/p/tQQi_o4N0YY
    Qjues
    Qjues
    OP
    Nov 3, 2025
    26
    @bv 日期设置成 10 月 31 号,time.Date(2025, time.October, 31, 0, 0, 0, 0, time.Local) 。并没有修复
    unco020511
    unco020511
    Nov 3, 2025
    27
    得用每月 1 号去减一天
    realJamespond
    realJamespond
    Nov 3, 2025
    28
    你这种不就是直接减 30 天的做法? 10 几年前 php 就遇到过
    littlefishzzz
    littlefishzzz
    Nov 3, 2025
    29
    @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
    skallz
    Nov 3, 2025
    30
    月份和年份本来就不能直接做加减,月份每月天数不一样,年份有闰年,一般系统的做法都是在产品说明里面写明一个月默认 30 天,一年默认 365 天,然后使用天数做加减的;其他加减月份和年份的库就算兼容了这些情况,但是实际表现的结果不统一,难以直观预测结果,反而会很奇怪,还不如提前告知用默认天数加减
    way2create
    way2create
    Nov 3, 2025
    31
    知道这个坑,感觉 PHP 那个文章已经说的很清楚了
    rekulas
    rekulas
    Nov 3, 2025
    32
    典型的日期 overflow 问题 一般用成熟的三方库比较好已经做了优化了,不然就得自己处理
    rrfeng
    rrfeng
    Nov 4, 2025
    33
    先定义好减一个月是个什么运算
    invzhi
    invzhi
    Nov 5, 2025
    34
    哈哈,用法不对
    iseki
    iseki
    Nov 9, 2025 via Android
    35
    我写了库 github.com/iseki0/goda 还没写完,打算在照抄 java.time 的基础上改造为符合 Go 习惯的 API 风格。也就是说会以符合 JSR-310 建模为导向。
    gouguoyin
    gouguoyin
    Nov 12, 2025
    36
    go-carbon 库提供了 SubMonthsNoOverflow/SubMonths,AddMonthsNoOverflow/AddMonths 等方法来处理月份溢出问题
    https://carbon.go-pkg.com/zh/usage/traveler.html#%E6%9C%88%E4%BB%BD%E6%97%85%E8%A1%8C
    iseki
    iseki
    Nov 12, 2025 via Android
    37
    我这边的也 https://v2ex.com/t/1172149 基本可用了
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   3671 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 81ms · UTC 05:20 · PVG 13:20 · LAX 21:20 · JFK 00:20
    ♥ Do have faith in what you're doing.