t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
s := t.Format("2006-01-02T15:04:05Z")
t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.FixedZone("a", 8*3600))
s1 := t1.Format("2006-01-02T15:04:05Z")
println(t.Unix())
println(t1.Unix())
println(s)
println(s1)
Go
大家可以想想这段代码的输出是什么:
1257894000
1257865200
2009-11-10T23:00:00Z
2009-11-10T23:00:00Z
Go
我们可以看到 t 和 t1 format 出来的字符串 s 和 s1 是一样的
但是 t 和 t1 两个时间之间相差了 8 个小时
最开始组内的小伙伴以为发生了灵异的事情,因为在他的印象中 Format 有的时候可以支持时区,有的时区无效必须要转成零时区进行处理
好在 Golang 是一个比较开放的语言我们可以很容易看到标准库中关于 time.Format 的实现
func (t Time) Format(layout string) string {
const bufSize = 64
var b []byte
max := len(layout) + 10
if max < bufSize {
var buf [bufSize]byte
b = buf[:0]
} else {
b = make([]byte, 0, max)
}
b = t.AppendFormat(b, layout)
return string(b)
}
Go
可以看到 Format 实现的一个重要的方法是 AppendFormat 我们来看下这个方法的实现:
// AppendFormat is like Format but appends the textual
// representation to b and returns the extended buffer.
func (t Time) AppendFormat(b []byte, layout string) []byte {
var (
name, offset, abs = t.locabs()
year int = -1
month Month
day int
yday int
hour int = -1
min int
sec int
)
// Each iteration generates one std value.
for layout != "" {
prefix, std, suffix := nextStdChunk(layout)
if prefix != "" {
b = append(b, prefix...)
}
if std == 0 {
break
}
layout = suffix
// Compute year, month, day if needed.
if year < 0 && std&stdNeedDate != 0 {
year, month, day, yday = absDate(abs, true)
yday++
}
// Compute hour, minute, second if needed.
if hour < 0 && std&stdNeedClock != 0 {
hour, min, sec = absClock(abs)
}
switch std & stdMask {
case stdYear:
y := year
if y < 0 {
y = -y
}
b = appendInt(b, y%100, 2)
case stdLongYear:
b = appendInt(b, year, 4)
case stdMonth:
b = append(b, month.String()[:3]...)
case stdLongMonth:
m := month.String()
b = append(b, m...)
case stdNumMonth:
b = appendInt(b, int(month), 0)
case stdZeroMonth:
b = appendInt(b, int(month), 2)
case stdWeekDay:
b = append(b, absWeekday(abs).String()[:3]...)
case stdLongWeekDay:
s := absWeekday(abs).String()
b = append(b, s...)
case stdDay:
b = appendInt(b, day, 0)
case stdUnderDay:
if day < 10 {
b = append(b, ' ')
}
b = appendInt(b, day, 0)
case stdZeroDay:
b = appendInt(b, day, 2)
case stdUnderYearDay:
if yday < 100 {
b = append(b, ' ')
if yday < 10 {
b = append(b, ' ')
}
}
b = appendInt(b, yday, 0)
case stdZeroYearDay:
b = appendInt(b, yday, 3)
case stdHour:
b = appendInt(b, hour, 2)
case stdHour12:
// Noon is 12PM, midnight is 12AM.
hr := hour % 12
if hr == 0 {
hr = 12
}
b = appendInt(b, hr, 0)
case stdZeroHour12:
// Noon is 12PM, midnight is 12AM.
hr := hour % 12
if hr == 0 {
hr = 12
}
b = appendInt(b, hr, 2)
case stdMinute:
b = appendInt(b, min, 0)
case stdZeroMinute:
b = appendInt(b, min, 2)
case stdSecond:
b = appendInt(b, sec, 0)
case stdZeroSecond:
b = appendInt(b, sec, 2)
case stdPM:
if hour >= 12 {
b = append(b, "PM"...)
} else {
b = append(b, "AM"...)
}
case stdpm:
if hour >= 12 {
b = append(b, "pm"...)
} else {
b = append(b, "am"...)
}
case stdISO8601TZ, stdISO8601ColonTZ, stdISO8601SecondsTZ, stdISO8601ShortTZ, stdISO8601ColonSecondsTZ, stdNumTZ, stdNumColonTZ, stdNumSecondsTz, stdNumShortTZ, stdNumColonSecondsTZ:
// Ugly special case. We cheat and take the "Z" variants
// to mean "the time zone as formatted for ISO 8601".
if offset == 0 && (std == stdISO8601TZ || std == stdISO8601ColonTZ || std == stdISO8601SecondsTZ || std == stdISO8601ShortTZ || std == stdISO8601ColonSecondsTZ) {
b = append(b, 'Z')
break
}
zone := offset / 60 // convert to minutes
absoffset := offset
if zone < 0 {
b = append(b, '-')
zone = -zone
absoffset = -absoffset
} else {
b = append(b, '+')
}
b = appendInt(b, zone/60, 2)
if std == stdISO8601ColonTZ || std == stdNumColonTZ || std == stdISO8601ColonSecondsTZ || std == stdNumColonSecondsTZ {
b = append(b, ':')
}
if std != stdNumShortTZ && std != stdISO8601ShortTZ {
b = appendInt(b, zone%60, 2)
}
// append seconds if appropriate
if std == stdISO8601SecondsTZ || std == stdNumSecondsTz || std == stdNumColonSecondsTZ || std == stdISO8601ColonSecondsTZ {
if std == stdNumColonSecondsTZ || std == stdISO8601ColonSecondsTZ {
b = append(b, ':')
}
b = appendInt(b, absoffset%60, 2)
}
case stdTZ:
if name != "" {
b = append(b, name...)
break
}
// No time zone known for this time, but we must print one.
// Use the -0700 format.
zone := offset / 60 // convert to minutes
if zone < 0 {
b = append(b, '-')
zone = -zone
} else {
b = append(b, '+')
}
b = appendInt(b, zone/60, 2)
b = appendInt(b, zone%60, 2)
case stdFracSecond0, stdFracSecond9:
b = formatNano(b, uint(t.Nanosecond()), std>>stdArgShift, std&stdMask == stdFracSecond9)
}
}
return b
}
Go
我们可以着重的去查看方法里有没有关于时区处理的逻辑,显而易见是有的:
这个函数简单的逻辑就是
把 模板 layout 分为多个 chunk 遍历所有的 chunk 进行渲染处理
这部分是关于时区 layout 处理的逻辑:
case stdISO8601TZ, stdISO8601ColonTZ, stdISO8601SecondsTZ, stdISO8601ShortTZ, stdISO8601ColonSecondsTZ, stdNumTZ, stdNumColonTZ, stdNumSecondsTz, stdNumShortTZ, stdNumColonSecondsTZ:
case stdTZ:
Go
只当有这些 时区的 chunk 才会进行时区处理,所以我们简单写的 Z 是不正确或者 Golang 不支持的时区定义表达方式
正确的时区格式模板如下:
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"
Flow
上面这个问题平时很少被关注,但一旦涉及到时区转换的时候就会发生各种 BUG 所以需要特别注意。