go time.Parse的一些小知识

本文探讨了Go语言中time.Parse函数在处理时区和时间布局模板时的细节。时区问题可以通过time.ParseInLocation解决,需要注意时区定义。时间布局模板遵循特定规则,如"2006年1月2日 下午3点4分5秒 周一",自定义布局时必须遵循这些规则。Go的内部解析方法逐个检查layout中的字符,以确定时间元素的位置。建议熟悉时间布局标准以避免解析不确定性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前都是写在笔记里,搬到csdn上。。。

go的time.Parse

在自己写东西时,遇到了时间转换的一些小问题。 首先是 time.Parse(layout, value), go语言提供的layout模版和我个人使用都略有差别, 转换使用很不习惯。 第二就是时区问题, 服务器的时区不一定是符合国内时区的。

时区问题

时区问题可以使用time.ParseInLocation, 但是涉及到时区定义。
查看源码定义(/time/format.go)

// 参数1还是layout
// 参数2是value
// 参数3比parse多了一个 "Location"
func ParseInLocation(layout, value string, loc *Location) (Time, error) {
	return parse(layout, value, loc, loc)
}

发现时区Location 是个结构体, 追进去看定义(/time/zoneinfo.go文件)

//go:generate env ZONEINFO=$GOROOT/lib/time/zoneinfo.zip go run genzabbrs.go -output zoneinfo_abbrs_windows.go
// 省略部分注释
type Location struct {
	name string
	zone []zone
	tx   []zoneTrans
	// 省略部分定义
}

重点是 go generate 的这一行, 是go自带的命令, 在运行时, 将后面的内容注释解析成命令执行. (还想了解更深的请自行查找)
其实需要的是 ZONEINFO=$GOROOT/lib/time/zoneinfo.zip, 也就是可以从这个路径找到对应的时区包文件。
然后在包中发现 Asia地区 关于中国的时区只有两个, 一个是Shanghai, 另一个是Chongqing。 一开始不明白的时候傻呵呵的写了 “Asia/Beijing” 然后发现并没有什么卵用。。

zoneinfo部分截图

oldTime := "2019-07-31T17:12:00Z"
location, error := time.LoadLocation("Asia/Chongqing")
time, _ := time.ParseInLocation(time.RFC3339, oldTime, location)
fmt.Println(time) // 2019-07-31 17:12:00 +0000 UTC

layout模版

time/format.go文件中包含所有的预定义 layout

const (
   ANSIC       = "Mon Jan _2 15:04:05 2006"
   UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
   RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
   RFC822      = "02 Jan 06 15:04 MST"
   RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
   RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
   RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
   RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
   RFC3339     = "2006-01-02T15:04:05Z07:00"
   RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
   Kitchen     = "3:04PM"
   // Handy time stamps.
   Stamp      = "Jan _2 15:04:05"
   StampMilli = "Jan _2 15:04:05.000"
   StampMicro = "Jan _2 15:04:05.000000"
   StampNano  = "Jan _2 15:04:05.000000000"
)

可以发现 所有的时间都指向了"2006年1月2日 下午3点4分5秒 周一"
目前听到的最多的声音是 “这是go语言的生日”。(目前我知道的最早的生日是2009-10-11), 但从源码中可以看到一些端倪:
从time.Parse方法进入, 会进入一个内部的方法, 通过查找layout关键词, 找到了解析的方法 nextStdChunk

func parse(layout, value string, defaultLocation, local *Location) (Time, error) {
...前面省略
prefix, std, suffix := nextStdChunk(layout)
...后面省略
}

nextStdChunk方法截图
解析方法就是逐个字母遍历我们传递的layout, (类似于java中 String.charAt()),
随机打开其中的几个

case 'J': // January, Jan
   		if len(layout) >= i+3 && layout[i:i+3] == "Jan" {
   			if len(layout) >= i+7 && layout[i:i+7] == "January" {
   				return layout[0:i], stdLongMonth, layout[i+7:]
   			}
   			if !startsWithLowerCase(layout[i+3:]) {
   				return layout[0:i], stdMonth, layout[i+3:]
   			}
   		}
case '3':
   		return layout[0:i], stdHour12, layout[i+1:]
case '5':
   		return layout[0:i], stdSecond, layout[i+1:]
case 'P': // PM
   		if len(layout) >= i+2 && layout[i+1] == 'M' {
   			return layout[0:i], stdPM, layout[i+2:]
   		}
case 'Z': // Z070000, Z07:00:00, Z0700, Z07:00,
   		if len(layout) >= i+7 && layout[i:i+7] == "Z070000" {
   			return layout[0:i], stdISO8601SecondsTZ, layout[i+7:]
   		}
   		if len(layout) >= i+9 && layout[i:i+9] == "Z07:00:00" {
   			return layout[0:i], stdISO8601ColonSecondsTZ, layout[i+9:]
   		}
   		if len(layout) >= i+5 && layout[i:i+5] == "Z0700" {
   			return layout[0:i], stdISO8601TZ, layout[i+5:]
   		}
   		if len(layout) >= i+6 && layout[i:i+6] == "Z07:00" {
   			return layout[0:i], stdISO8601ColonTZ, layout[i+6:]
   		}
   		if len(layout) >= i+3 && layout[i:i+3] == "Z07" {
   			return layout[0:i], stdISO8601ShortTZ, layout[i+3:]
   		}

一切都豁然开朗, 解析其实是 将固定的字母/数字/符号 作为特殊的标志,作为特殊的位置, 在结合强力的分析, 通过截取固定的位数来判断具体属于哪种layout及每个时段的表现形式
例如
字母 J 代表的是 月份(month)
数字 3 代表的是 小时(hour)
数字5 代表的是秒
字母P/p 代表的是 PM/pm
字母Z 代表的是具体展现形式。
and so on。

可以看到, go在内部解析是将 字母和数字分割开来,并且互相不会有影响。 但如果我们随意编写时间layout, 比如"2019-07-31 12:32:12"。结果就是比较难预测的。

所有的解析字段也在time/format.go里有常量定义:

const (
	_                        = iota
	stdLongMonth             = iota + stdNeedDate  // "January"
	stdMonth                                       // "Jan"
	stdNumMonth                                    // "1"
	stdZeroMonth                                   // "01"
	stdLongWeekDay                                 // "Monday"
	stdWeekDay                                     // "Mon"
	stdDay                                         // "2"
	stdUnderDay                                    // "_2"
	stdZeroDay                                     // "02"
	stdHour                  = iota + stdNeedClock // "15"
	stdHour12                                      // "3"
	stdZeroHour12                                  // "03"
	stdMinute                                      // "4"
	stdZeroMinute                                  // "04"
	stdSecond                                      // "5"
	stdZeroSecond                                  // "05"
	stdLongYear              = iota + stdNeedDate  // "2006"
	stdYear                                        // "06"
	stdPM                    = iota + stdNeedClock // "PM"
	stdpm                                          // "pm"
	stdTZ                    = iota                // "MST"
	stdISO8601TZ                                   // "Z0700"  // prints Z for UTC
	stdISO8601SecondsTZ                            // "Z070000"
	stdISO8601ShortTZ                              // "Z07"
	stdISO8601ColonTZ                              // "Z07:00" // prints Z for UTC
	stdISO8601ColonSecondsTZ                       // "Z07:00:00"
	stdNumTZ                                       // "-0700"  // always numeric
	stdNumSecondsTz                                // "-070000"
	stdNumShortTZ                                  // "-07"    // always numeric
	stdNumColonTZ                                  // "-07:00" // always numeric
	stdNumColonSecondsTZ                           // "-07:00:00"
	stdFracSecond0                                 // ".0", ".00", ... , trailing zeros included
	stdFracSecond9                                 // ".9", ".99", ..., trailing zeros omitted

	stdNeedDate  = 1 << 8             // need month, day, year
	stdNeedClock = 2 << 8             // need hour, minute, second
	stdArgShift  = 16                 // extra argument in high bits, above low stdArgShift
	stdMask      = 1<<stdArgShift - 1 // mask out argument
)

所以在自定义layout的时候 一定要遵循这个时间规范, "2006年1月2日 下午3点4分5秒 周一"
或者一个小顺口溜(来源于studygolang.com的徐大) : “1月2日3点4分5秒6年7时区” 刚好是1234567

像我个人常用的时间表示类似于 -> “2019-08-01 13:20:05”

// java写法
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.format(date);

// php写法
(new Datetime())->modify("")->format('Y-m-d H:i:s');

// go写法
oldTime := "2019-07-31T17:12:00Z"
// 当然 不用时区 直接time.Parse()也是可以的
location, error := time.LoadLocation("Asia/Chongqing")
time, _ := time.ParseInLocation(time.RFC3339, oldTime, location)
time.Format("2006-01-02 15:04:05")

总结

go的这个时间解析layout的设计还是非常巧妙的, 虽然对新手仿佛不是那么友好~ 尤其一上来这个时间戳让人有点摸不到头脑。
更多的使用例子在 /time/example_test.go文件中, 也有许多注释说明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值