当前位置: 首页 > 后端技术 > PHP

混淆Go时间.AddDate

时间:2023-03-30 03:44:10 PHP

我们经常使用Go时间包AddDate()来计算日期。而它得到的结果,往往可能会超出我们的“预期”。(为什么期望用引号引起来,因为我们的期望可能是模糊和有偏见的)。假设今天是10月31日,也就是10月的最后一天,我们想通过AddDate()计算出下个月的最后一天。今天:=time.Date(2022,10,31,0,0,0,0,time.Local)nextDay:=today.AddDate(0,1,0)fmt.Println(nextDay.Format("20060102"))//Output:20221201结果输出:20221201,不是我们预期的11月30日,也就是下个月的最后一天。GoTime包中是这样处理的:AddDate()将月份加1,即变成11-31,转换成对应的天数,最后转换成对应的纳秒数存入时间对象;输出时,Format()会输出标准日期,Time中的纳秒会被转换成12-01而不是11-31,因为这一天不存在;只要涉及大小月的最后一天,就会出现这个问题。今天:=time.Date(2022,3,31,0,0,0,0,time.Local)d:=today.AddDate(0,-1,0)fmt.Println(d.Format("20060102"))//20220303today:=time.Date(2022,3,31,0,0,0,0,time.Local)d:=today.AddDate(0,1,0)fmt.Println(d.Format("20060102"))//20220501today:=time.Date(2022,10,31,0,0,0,0,time.Local)d:=today.AddDate(0,-1,0)fmt.Println(d.Format("20060102"))//20221001today:=time.Date(2022,10,31,0,0,0,0,time.Local)d:=today.AddDate(0,1,0)fmt.Println(d.Format("20060102"))//20221201源码分析看GoTime包的具体源码,还是以一开始的10-31+1个月的例子作为用例。AddDate(),先加上month+1,再调用Date()进行处理。//time/time.gofunc(tTime)AddDate(yearsint,monthsint,daysint)Time{year,month,day:=t.Date()//获取当前年月日时分秒:=t.Clock()//获取当前时分秒returnDate(year+years,month+Month(months),day+days,hour,min,sec,int(t.nsec()),t.Location())}Date()此时传入的参数是年2020月11日31小时,min、sec、nsec分别是运行时的小时、分钟、秒和纳秒。d计算绝对纪元前到今天的天数:**d=今年前的天数+年初到本月的天数+月初的天数到今天;**最后,将d转换成纳秒+当天流逝的纳秒存储在Time对象中。//time/time.gofuncDate(yearint,monthMonth,day,hour,min,sec,nsecint,loc*Location)Time{...//计算自绝对纪元以来的天数。d:=daysSinceEpoch(year)//添加本月之前的天数。d+=uint64(daysBefore[month-1])ifisLeap(year)&&month>=March{d++//February29}//添加今天之前的天数。d+=uint64(day-1)//添加今天经过的时间。abs:=d*secondsPerDayabs+=uint64(hour*secondsPerHour+min*secondsPerMinute+sec)...返回t}到Date()输入2022-11-31和输入2022-12-01将得到相同的d(天数)。它们都在底层存储相同的数据。在Format()中也不例外将2022-11-31的时间格式化为2022-12-01。当然,输出必须显示人们可以理解的常规标准日期Well。//2022-11-31d=2022年前的天数+1月到10月的总天数+30天//2022-12-01d=2022年前的天数+1月到11月的总天数+0天=2022年前的天数+一月到十月的总天数+30天+0天你甚至可以在Date()中输入一个非标准日期2022-11-35,它和标准日期2022-12-05,你会得到相同的d(天数)。“非标准日期”和“标准日期”就像天平的两端。尽管形式不同,但它们的实际质量(d天)是相同的。记住这句话,以后会有用的。预期偏差我们想通了原理,但还是不能接受这个结果。这个结果是Go的bug吗?还是GoTime包懒惰?然而,并不是,恰恰是我们的“期望”出了问题。通常我们预计10-30+1个月为11-30天,这是合理的。那为什么我们还期望10-31+1个月也是11-30?仅仅因为10-31是当月的最后一天,我们是否也期望+1个月后是下个月的最后一天?10-30和10-31这两个日期相差一天。在执行相同的+1个月操作后,它们变成同一天。这和1+10=2+10是一样的结果,显然不合理。Go当前的处理结果是正确的,他在AddDate()注释中也注明会处理“溢出”的情况。而且不仅Go语言是这样处理的,PHP也是这样处理的。看鸟哥文章-风雪一角的迷惑strtotime。我明白如何解决问题,但是如果我只想获取上/下个月的最后一天怎么办?利用前面源码分析阶段提到的“平衡原则”,我们可以得到我们想要的结果。today:=time.Date(2022,10,31,0,0,0,0,time.Local)d:=today.Day()//上个月的最后一天//10-00等于9-30day1:=today.AddDate(0,0,-d)fmt.Println(day1.Format("20060102"))//下个月的最后一天//12-00等于11-30day2:=today.AddDate(0,2,-d)fmt.Println(day2.Format("20060102"))//20220930//20221130结语一开始是看鸟哥的文章发现这个问题的,以为是是PHP的一个“坑”,并没有深入思考。今天在Go语言中又遇到了这个问题。转念一想,date函数本来应该这么设计的。是因为我们对日期函数了解不够,导致了错误的“预期”。文章来自令人困惑的Gotime.AddDate