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

2025 年 10 月 31 日
 Qjues

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

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

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'))
5100 次点击
所在节点    Go 编程语言
37 条回复
Sezxy
2025 年 10 月 31 日
每月 1 号再减
body007
2025 年 10 月 31 日
学到新知识,就像我定每月最后一天的闹钟都是定每月 1 号提前一天提醒一样,这样可以消除每月天数不同的问题,所以可以按下面方式做吧。

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

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

```
cppc
2025 年 11 月 1 日
func (t Time) AddDate(years int, months int, days int) Time

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


python 的 relativedelta 应该没问题(代码是 copilot 写的)
mcfog
2025 年 11 月 1 日
歪题:PHP 有丧心病狂的 last day previous month
https://3v4l.org/eN9Ip
ksedz
2025 年 11 月 1 日
10.31 -1 month 到 10.1 在语义上就是不对的.
与其说 9.31 不存在时的处理问题,不如说是 -1 month 处理的太简陋了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://v2ex.xtra.eu.org/t/1169781

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX