今天生产环境出现了个奇怪的异常,挺纳闷的,毕竟这个服务没有怎么迭代。经过排查定位发现,居然是某些特殊日子才会出现的问题。

1
RetentionMonths = 3 MonthTablePrefix = "jobs_"

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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 表。

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

1
2
3
4
5
6
=== 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 有两个,问题就在这里了。

1
2
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 )

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

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

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

js moment 试了是正常的

1
console.log(moment(new Date()).subtract(1, 'months').format('YYYY-MM-DD'))

我刚想甩锅给 Go 时间库做得差呢,结果一看,人家文档上写了:

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.