Golang Time Format 中的时区问题

golang time format zone bugs

foreversmart write on 2019-10-22
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 所以需要特别注意。

「真诚赞赏,手留余香」

Foreversmart

真诚赞赏,手留余香

使用微信扫描二维码完成支付