今天生产环境出现比较多的异常,挺纳闷的,毕竟最近该服务没有怎么迭代。
经过排查定位发现,居然是某些特殊日子才会出现的问题。
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'))
1
Goooooos 4 天前 via Android 每月 1 号再减
|
2
body007 4 天前
学到新知识,就像我定每月最后一天的闹钟都是定每月 1 号提前一天提醒一样,这样可以消除每月天数不同的问题,所以可以按下面方式做吧。
now := time.Now() now = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) |
3
superjojo 4 天前
做过日期类软件,老多特殊情况了
|
4
moefishtang 4 天前 万圣节等于圣诞节是吧😂
Oct(31)==Dec(25) |
5
liaohongxing 4 天前 via Android
我都是用 carbon 时间库处理
|
6
lovelylain 4 天前 via Android 当月 0 号就是上个月最后一天,了解这一点要方便很多,否则容易写出 bug 或不简洁代码。
|
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. 咱写代码时理应能够意识到这个问题,意识到这个问题后理应知道去查阅文档。 |
8
ZeroDu 4 天前
get 新知识,之前写 java ,里面就没这个问题
|
9
iseki 4 天前
我刚想甩锅给 Go 时间库做得差呢,结果一看,人家写了,你不看······
|
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. 如果写代码时不注意,换一个需求一样可能踩坑
|
11
joey9696 4 天前
时间边界 可以用 lancet 这个库
|
12
mmdsun 4 天前 via iPhone
Java 10-31 减去一个月为 09-31, 但是又因为 9 月没有 31 号,就会是 9 月最后一天。如果没记错的话。这样比较合理。
|
13
wenrouxiaozhu 4 天前
|
14
liuliuliuliu PRO ```csharp
var date = new DateTime(2025,10,31); var date2 = date.AddMonths(-1); Console.WriteLine(date2); // 2025-9-30 00:00:00 ``` |
15
cppc 4 天前
func (t Time) AddDate(years int, months int, days int) Time
这个 API 的形态就很烧脑,因为月份和日期都会有一个逻辑边界。应该像别的语言那样,调整年月日设计成三个独立的方法。 |
16
pike0002 4 天前
我记得我们当时针对这个自己封装了个 AddDate()方法
|
17
sunwq 4 天前
PHP 也出现了类似的问题😂
|
18
rming 4 天前 via iPhone
|
19
mcfog 4 天前 歪题:PHP 有丧心病狂的 last day previous month
https://3v4l.org/eN9Ip |
20
ksedz 4 天前
10.31 -1 month 到 10.1 在语义上就是不对的.
与其说 9.31 不存在时的处理问题,不如说是 -1 month 处理的太简陋了。 |
21
EminemW 3 天前
哈哈哈,我之前也遇到这个问题,特地写了个 AddMonth 方法,防止后面的人踩坑
|
22
AV1 3 天前
做月份加减法本来就很奇怪,或者说没有一个很通用的计算方法,比如 3 月 31 日减一个月,是希望得到 2 月几日呢?
|
24
iseki 3 天前 via Android
@archxm 这个就看经验咯,前两天我也被某个自以为了解的 API 坑了一下,Go 里不是有个 exec/Command.Wait 嘛,我打死也没想到这个 API 会顺手把我 StdoutPipe 来的管子给关了,查了好一会儿才发现。
|
25
bv 2 天前
这个问题在高版本中是不是已经修复了: https://go.dev/play/p/tQQi_o4N0YY
|
26
Qjues OP |
27
unco020511 2 天前
得用每月 1 号去减一天
|
28
realJamespond 2 天前
你这种不就是直接减 30 天的做法? 10 几年前 php 就遇到过
|
29
littlefishzzz 2 天前
@wenrouxiaozhu pendulum 库可以
![]() ``` >>> import pendulum >>> dt = pendulum.datetime(2025, 10, 31) >>> dt.subtract(months=1) DateTime(2025, 9, 30, 0, 0, 0, tzinfo=Timezone('UTC')) ``` |
30
skallz 2 天前
月份和年份本来就不能直接做加减,月份每月天数不一样,年份有闰年,一般系统的做法都是在产品说明里面写明一个月默认 30 天,一年默认 365 天,然后使用天数做加减的;其他加减月份和年份的库就算兼容了这些情况,但是实际表现的结果不统一,难以直观预测结果,反而会很奇怪,还不如提前告知用默认天数加减
|
31
way2create 2 天前
知道这个坑,感觉 PHP 那个文章已经说的很清楚了
|
32
rekulas 1 天前
典型的日期 overflow 问题 一般用成熟的三方库比较好已经做了优化了,不然就得自己处理
|
33
rrfeng 1 天前
先定义好减一个月是个什么运算
|
34
invzhi 5 小时 37 分钟前
哈哈,用法不对
|