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

  •  
  •   Qjues · 21 小时 2 分钟前 · 1529 次点击

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

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

    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'))
    
    20 条回复    2025-11-01 13:03:04 +08:00
    Goooooos
        1
    Goooooos  
       20 小时 59 分钟前 via Android
    每月 1 号再减
    body007
        2
    body007  
       20 小时 50 分钟前
    学到新知识,就像我定每月最后一天的闹钟都是定每月 1 号提前一天提醒一样,这样可以消除每月天数不同的问题,所以可以按下面方式做吧。

    now := time.Now()
    now = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
    superjojo
        3
    superjojo  
       20 小时 50 分钟前
    做过日期类软件,老多特殊情况了
    moefishtang
        4
    moefishtang  
       20 小时 29 分钟前
    万圣节等于圣诞节是吧😂
    Oct(31)==Dec(25)
    liaohongxing
        5
    liaohongxing  
       19 小时 51 分钟前 via Android
    我都是用 carbon 时间库处理
    lovelylain
        6
    lovelylain  
       19 小时 44 分钟前 via Android
    当月 0 号就是上个月最后一天,了解这一点要方便很多,否则容易写出 bug 或不简洁代码。
    iseki
        7
    iseki  
       19 小时 33 分钟前
    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  
       19 小时 32 分钟前
    get 新知识,之前写 java ,里面就没这个问题
    iseki
        9
    iseki  
       19 小时 32 分钟前
    我刚想甩锅给 Go 时间库做得差呢,结果一看,人家写了,你不看······
    iseki
        10
    iseki  
       19 小时 30 分钟前
    @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  
       19 小时 16 分钟前
    时间边界 可以用 lancet 这个库
    mmdsun
        12
    mmdsun  
       16 小时 49 分钟前 via iPhone
    Java 10-31 减去一个月为 09-31, 但是又因为 9 月没有 31 号,就会是 9 月最后一天。如果没记错的话。这样比较合理。
    wenrouxiaozhu
        13
    wenrouxiaozhu  
       16 小时 29 分钟前
    liuliuliuliu
        14
    liuliuliuliu  
    PRO
       16 小时 22 分钟前
    ```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  
       15 小时 31 分钟前
    func (t Time) AddDate(years int, months int, days int) Time

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


    python 的 relativedelta 应该没问题(代码是 copilot 写的)
    mcfog
        19
    mcfog  
       3 小时 38 分钟前
    歪题:PHP 有丧心病狂的 last day previous month
    https://3v4l.org/eN9Ip
    ksedz
        20
    ksedz  
       2 小时 35 分钟前
    10.31 -1 month 到 10.1 在语义上就是不对的.
    与其说 9.31 不存在时的处理问题,不如说是 -1 month 处理的太简陋了。
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   2627 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 07:38 · PVG 15:38 · LAX 00:38 · JFK 03:38
    ♥ Do have faith in what you're doing.