之前都是写在笔记里,搬到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” 然后发现并没有什么卵用。。
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)
...后面省略
}
解析方法就是逐个字母遍历我们传递的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文件中, 也有许多注释说明。