1. 前言
时间包括时间值和时区, 没有包含时区信息的时间是不完整的、有歧义的. 和外界传递或解析时间数据时, 应当像HTTP协议或unix-timestamp那样, 使用没有时区歧义的格式, 如果使用某些没有包含时区的非标准的时间表示格式(如yyyy-mm-dd HH:MM:SS), 是有隐患的, 因为解析时会使用场景的默认设置, 如系统时区, 数据库默认时区可能引发事故. 确保服务器系统、数据库、应用程序使用统一的时区, 如果因为一些历史原因, 应用程序各自保持着不同时区, 那么编程时要小心检查代码, 知道时间数据在使用不同时区的程序之间交换时的行为. 第三节会详细解释go程序在不同场景下time.Time的行为.
2. Time的数据结构
go1.9之前, time.Time的定义为
type Time struct {
// sec gives the number of seconds elapsed since
// January 1, year 1 00:00:00 UTC.
sec int64
// nsec specifies a non-negative nanosecond
// offset within the second named by Seconds.
// It must be in the range [0, 999999999].
nsec int32
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// The nil location means UTC.
// All UTC times are represented with loc==nil, never loc==&utcLoc.
loc *Location
}
sec表示从公元1年1月1日00:00:00UTC到要表示的整数秒数, nsec表示余下的纳秒数, loc表示时区. sec和nsec处理没有歧义的时间值, loc处理偏移量.
因为2017年闰一秒, 国际时钟调整, Go程序两次取time.Now()相减的时间差得到了意料之外的负数, 导致cloudFlare的CDN服务中断, 详见https://blog.cloudflare.com/how-and-why-the-leap-second-affected-cloudflare-dns/, go1.9在不影响已有应用代码的情况下修改了time.Time的实现. go1.9的time.Time定义为
// A Time represents an instant in time with nanosecond precision.
//
// Programs using times should typically store and pass them as values,
// not pointers. That is, time variables and struct fields should be of
// type time.Time, not *time.Time.
//
// A Time value can be used by multiple goroutines simultaneously except
// that the methods GobDecode, UnmarshalBinary, UnmarshalJSON and
// UnmarshalText are not concurrency-safe.
//
// Time instants can be compared using the Before, After, and Equal methods.
// The Sub method subtracts two instants, producing a Duration.
// The Add method adds a Time and a Duration, producing a Time.
//
// The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC.
// As this time is unlikely to come up in practice, the IsZero method gives
// a simple way of detecting a time that has not been initialized explicitly.
//
// Each Time has associated with it a Location, consulted when computing the
// presentation form of the time, such as in the Format, Hour, and Year methods.
// The methods Local, UTC, and In return a Time with a specific location.
// Changing the location in this way changes only the presentation; it does not
// change the instant in time being denoted and therefore does not affect the
// computations described in earlier paragraphs.
//
// Note that the Go == operator compares not just the time instant but also the
// Location and the monotonic clock reading. Therefore, Time values should not
// be used as map or database keys without first guaranteeing that the
// identical Location has been set for all values, which can be achieved
// through use of the UTC or Local method, and that the monotonic clock reading
// has been stripped by setting t = t.Round(0). In general, prefer t.Equal(u)
// to t == u, since t.Equal uses the most accurate comparison available and
// correctly handles the case when only one of its arguments has a monotonic
// clock reading.
//
// In addition to the required “wall clock” reading, a Time may contain an optional
// reading of the current process's monotonic clock, to provide additional precision
// for comparison or subtraction.
// See the “Monotonic Clocks” section in the package documentation for details.
//
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// unsigned wall seconds since Jan 1 year 1885, and ext holds a
// signed 64-bit monotonic clock reading, nanoseconds since process start.
wall uint64
ext int64
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// The nil location means UTC.
// All UTC times are represented with loc==nil, never loc==&utcLoc.
loc *Location
}
3. time的行为
构造时间-获取现在时间-time.Now(), time.Now()使用本地时间, time.Local即本地时区, 取决于运行的系统环境设置, 优先取”TZ”这个环境变量, 然后取/etc/localtime, 都取不到就用UTC兜底.
func Now() Time { sec, nsec := now() return Time{sec + unixToInternal, nsec, Local} }
构造时间-获取某一时区的现在时间-time.Now().In(), Time结构体的
In()
方法仅设置loc, 不会改变时间值. 特别地, 如果是获取现在的UTC时间, 可以使用Time.Now().UTC().
时区不能为nil. time包中只有两个时区变量time.Local和time.UTC. 其他时区变量有两种方法取得, 一个是通过time.LoadLocation函数根据时区名字加载, 时区名字见IANA Time Zone database, LoadLocation首先查找系统zoneinfo, 然后查找$GOROOT/lib/time/zoneinfo.zip
.另一个是在知道时区名字和偏移量的情况下直接调用time.FixedZone("$zonename", $offsetSecond)
构造一个Location对象.// In returns t with the location information set to loc. // // In panics if loc is nil. func (t Time) In(loc *Location) Time { if loc == nil { panic("time: missing Location in call to Time.In") } t.setLoc(loc) return t } // LoadLocation returns the Location with the given name. // // If the name is "" or "UTC", LoadLocation returns UTC. // If the name is "Local", LoadLocation returns Local. // // Otherwise, the name is taken to be a location name corresponding to a file // in the IANA Time Zone database, such as "America/New_York". // // The time zone database needed by LoadLocation may not be // present on all systems, especially non-Unix systems. // LoadLocation looks in the directory or uncompressed zip file // named by the ZONEINFO environment variable, if any, then looks in // known installation locations on Unix systems, // and finally looks in $GOROOT/lib/time/zoneinfo.zip. func LoadLocation(name string) (*Location, error) { if name == "" || name == "UTC" { return UTC, nil } if name == "Local" { return Local, nil } if zoneinfo != "" { if z, err := loadZoneFile(zoneinfo, name); err == nil { z.name = name return z, nil } } return loadLocation(name) }
构造时间-手动构造时间-time.Date(), 传入年元日时分秒纳秒和时区变量Location构造一个时间. 得到的是指定location的时间.
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time { if loc == nil { panic("time: missing Location in call to Date") } ..... }
- 构造时间-从unix时间戳中构造时间, time.Unix(), 传入秒和纳秒构造.
序列化反序列化时间-文本和JSON, fmt.Sprintf,fmt.SScanf, json.Marshal, json.Unmarshal时的, 使用的时间格式均包含时区信息, 序列化使用RFC3339Nano()”2006-01-02T15:04:05.999999999Z07:00”, 反序列化使用RFC3339()”2006-01-02T15:04:05Z07:00”, 反序列化没有纳秒值也可以正常序列化成功.
// String returns the time formatted using the format string // "2006-01-02 15:04:05.999999999 -0700 MST" func (t Time) String() string { return t.Format("2006-01-02 15:04:05.999999999 -0700 MST") } // MarshalJSON implements the json.Marshaler interface. // The time is a quoted string in RFC 3339 format, with sub-second precision added if present. func (t Time) MarshalJSON() ([]byte, error) { if y := t.Year(); y < 0 || y >= 10000 { // RFC 3339 is clear that years are 4 digits exactly. // See golang.org/issue/4556#c15 for more discussion. return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]") } b := make([]byte, 0, len(RFC3339Nano)+2) b = append(b, '"') b = t.AppendFormat(b, RFC3339Nano) b = append(b, '"') return b, nil } // UnmarshalJSON implements the json.Unmarshaler interface. // The time is expected to be a quoted string in RFC 3339 format. func (t *Time) UnmarshalJSON(data []byte) error { // Ignore null, like in the main JSON package. if string(data) == "null" { return nil } // Fractional seconds are handled implicitly by Parse. var err error *t, err = Parse(`"`+RFC3339+`"`, string(data)) return err }
序列化反序列化时间-HTTP协议中的date, 统一GMT, 代码位于net/http/server.go:878
// TimeFormat is the time format to use when generating times in HTTP // headers. It is like time.RFC1123 but hard-codes GMT as the time // zone. The time being formatted must be in UTC for Format to // generate the correct format. // // For parsing this time format, see ParseTime. const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
序列化反序列化时间-
time.Format("$layout")
,time.Parse("$layout","$value")
,time.ParseInLocation("$layout","$value","$Location")
time.Format("$layout")
格式化时间时, 时区会参与计算. 调time.Time的Year()Month()Day()等获取年月日等时时区会参与计算, 得到一个使用偏移量修正过的正确的时间字符串, 若$layout
有指定显示时区, 那么时区信息会体现在格式化后的时间字符串中. 如果$layout
没有指定显示时区, 那么字符串只有时间没有时区, 时区是隐含的, time.Time对象中的时区.time.Parse("$layout","$value")
, 若$layout
有指定显示时区, 那么时区信息会体现在格式化后的time.Time对象. 如果$layout
没有指定显示时区, 那么使用会认为这是一个UTC时间, 时区是UTC.time.ParseInLocation("$layout","$value","$Location")
使用传参的时区解析时间, 建议用这个, 没有歧义.// Parse parses a formatted string and returns the time value it represents. // The layout defines the format by showing how the reference time, // defined to be // Mon Jan 2 15:04:05 -0700 MST 2006 // would be interpreted if it were the value; it serves as an example of // the input format. The same interpretation will then be made to the // input string. // // Predefined layouts ANSIC, UnixDate, RFC3339 and others describe standard // and convenient representations of the reference time. For more information // about the formats and the definition of the reference time, see the // documentation for ANSIC and the other constants defined by this package. // Also, the executable example for time.Format demonstrates the working // of the layout string in detail and is a good reference. // // Elements omitted from the value are assumed to be zero or, when // zero is impossible, one, so parsing "3:04pm" returns the time // corresponding to Jan 1, year 0, 15:04:00 UTC (note that because the year is // 0, this time is before the zero Time). // Years must be in the range 0000..9999. The day of the week is checked // for syntax but it is otherwise ignored. // // In the absence of a time zone indicator, Parse returns a time in UTC. // // When parsing a time with a zone offset like -0700, if the offset corresponds // to a time zone used by the current location (Local), then Parse uses that // location and zone in the returned time. Otherwise it records the time as // being in a fabricated location with time fixed at the given zone offset. // // No checking is done that the day of the month is within the month's // valid dates; any one- or two-digit value is accepted. For example // February 31 and even February 99 are valid dates, specifying dates // in March and May. This behavior is consistent with time.Date. // // When parsing a time with a zone abbreviation like MST, if the zone abbreviation // has a defined offset in the current location, then that offset is used. // The zone abbreviation "UTC" is recognized as UTC regardless of location. // If the zone abbreviation is unknown, Parse records the time as being // in a fabricated location with the given zone abbreviation and a zero offset. // This choice means that such a time can be parsed and reformatted with the // same layout losslessly, but the exact instant used in the representation will // differ by the actual zone offset. To avoid such problems, prefer time layouts // that use a numeric zone offset, or use ParseInLocation. func Parse(layout, value string) (Time, error) { return parse(layout, value, UTC, Local) } // ParseInLocation is like Parse but differs in two important ways. // First, in the absence of time zone information, Parse interprets a time as UTC; // ParseInLocation interprets the time as in the given location. // Second, when given a zone offset or abbreviation, Parse tries to match it // against the Local location; ParseInLocation uses the given location. func ParseInLocation(layout, value string, loc *Location) (Time, error) { return parse(layout, value, loc, loc) } func parse(layout, value string, defaultLocation, local *Location) (Time, error) { ..... }
序列化反序列化时间-go-sql-driver/mysql中的时间处理.
MySQL驱动解析时间的前提是连接字符串加了parseTime和loc, 如果parseTime为false, 会把mysql的date类型变成[]byte/string自行处理, parseTime为true才处理时间, loc指定MySQL中存储时间数据的时区, 如果没有指定loc, 用UTC. 序列化和反序列化均使用连接字符串中的设定的loc, SQL语句中的time.Time类型的参数的时区信息如果和loc不同, 则会调用t.In(loc)
方法转时区.解析连接字符串的代码位于parseDSNParams函数https://github.com/go-sql-driver/mysql/blob/master/dsn.go#L467-L490
// Time Location case "loc": if value, err = url.QueryUnescape(value); err != nil { return } cfg.Loc, err = time.LoadLocation(value) if err != nil { return } // time.Time parsing case "parseTime": var isBool bool cfg.ParseTime, isBool = readBool(value) if !isBool { return errors.New("invalid bool value: " + value) }
解析SQL语句中time.Time类型的参数的代码位于mysqlConn.interpolateParams方法https://github.com/go-sql-driver/mysql/blob/master/connection.go#L230-L273
case time.Time: if v.IsZero() { buf = append(buf, "'0000-00-00'"...) } else { v := v.In(mc.cfg.Loc) v = v.Add(time.Nanosecond * 500) // To round under microsecond year := v.Year() year100 := year / 100 year1 := year % 100 month := v.Month() day := v.Day() hour := v.Hour() minute := v.Minute() second := v.Second() micro := v.Nanosecond() / 1000 buf = append(buf, []byte{ '\'', digits10[year100], digits01[year100], digits10[year1], digits01[year1], '-', digits10[month], digits01[month], '-', digits10[day], digits01[day], ' ', digits10[hour], digits01[hour], ':', digits10[minute], digits01[minute], ':', digits10[second], digits01[second], }...) if micro != 0 { micro10000 := micro / 10000 micro100 := micro / 100 % 100 micro1 := micro % 100 buf = append(buf, []byte{ '.', digits10[micro10000], digits01[micro10000], digits10[micro100], digits01[micro100], digits10[micro1], digits01[micro1], }...) } buf = append(buf, '\'') }
从MySQL数据流中解析时间的代码位于textRows.readRow方法https://github.com/go-sql-driver/mysql/blob/master/packets.go#L772-L777, 注意只要MySQL连接字符串设置了parseTime=true, 就会解析时间, 不管你是用string还是time.Time接收的.
if !isNull { if !mc.parseTime { continue } else { switch rows.rs.columns[i].fieldType { case fieldTypeTimestamp, fieldTypeDateTime, fieldTypeDate, fieldTypeNewDate: dest[i], err = parseDateTime( string(dest[i].([]byte)), mc.cfg.Loc, ) if err == nil { continue } default: continue } } }
4. time时区处理不当案例
有个服务频繁使用最新汇率, 所以缓存了最新汇率对象, 汇率对象的过期时间设为第二天北京时间零点, 汇率过期则从数据库中去最新汇率, 设置过期时间的代码如下:
var startTime string = time.Now().UTC().Add(8 * time.Hour).Format("2006-01-02") tm2, _ := time.Parse("2006-01-02", startTime) lastTime = tm2.Unix() + 24*60*60
这段代码使用了time.Parse, 如果时间格式中没有指定时区, 那么会得到使用本地时区下的第二天零点, 服务器时区设置为UTC0, 于是汇率缓存在UTC零点即北京时间八点才更新.
公共库中有一个GetBjTime()方法, 注释写着将服务器UTC转成北京时间, 代码如下
// 原版 func GetBjTime() time.Time { // 将服务器UTC转成北京时间 uTime := time.Now().UTC() dur, _ := time.ParseDuration("+8h") return uTime.Add(dur) } // 改 func GetBjTime() time.Time { // 将服务器UTC转成北京时间 uTime := time.Now() return uTime.In(time.FixedZone("CST", 8*60*60)) }
同事用这个方法将得到的time.Time参与计算, 发现多了8个小时. 觉得有问题, 同事和我讨论了之后, 我们得出结论后就大意地直接把原有函数改了, 我们都没有意识到这是个非常危险操作, 只所以危险是因为这个函数已经在很多服务的代码里用着(要稳!不能乱动公共库!!!). 之前用这个函数是因为老Java项目运行在时区为东八区的系统上, 大量代码使用东八区时间, 但数据库MySQL时区设置为UTC, go项目也运行在UTC时区. 也就是说, Java项目在把时区为UTC数据库当做是东八区来用, Java程序往MySQL写东八区的时间字符串, 在sequel软件中看表内容时虽然字符串是一样的, 但其实内部是UTC的时间, go代码的mysql连接字符串中loc选项为空, 就会使用UTC时区去解析数据, 拿到的数据会多八个小时. 例如Java代码往mysql插入一条”2017-10-29 22:00:00”数据本意是东八区2017年10月29日22点, 但在MySQL内部看来, 这是UTC的2017年10月29日22点, 换算成东八区时间为2017年10月30日6点, 如果其它程序解析时认为时间数据是MySQL的UTC时区, 那么会得到一个错误的时间. 所以才会在GO中要往Java代码创建的表写入数据时用
time.Now().UTC().Add(time.Hour*8)
直接相加八小时使得Java项目行为一致, 拿UTC的数据库存东八区时间.后面想想, 面对这种数据库中有时区不一致数据的情况, 在没有办法统一UTC时区的情况下, 应当使用MySQL时间字符串而不是time.Time来传递以避免时区隐含转换问题, 写入时参数传string类型的时间字符串, 解析时先拿到时间字符串, 然后自行判断建表时这个字段用的是东八区的时间字符串还是UTC时间字符串进行time.ParseInLocation得到时间对象, MySQL连接字符串的parseTime选项要设置为false. 比如我想在MySQL中存东八区的当前时间, SQL参数用Format后的字符串而不是传time.Time, 原版的
time.Now().UTC().Add(time.Hour*8).Format("2006-01-02 15:04:05")
和修改的time.Now().In(time.FixedZone("CST", 8*60*60))
的输出将是一样, 但后者是正确的东八区现在时间. 原版的GetBjTime()返回time.Time可能用GetBeijingNowTimeString返回string更能体现本意吧.
5. 时间有关的标准
-
协调世界时(英语:Coordinated Universal Time,法语:Temps Universel Coordonné,简称UTC)是最主要的世界时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林尼治标准时间。中华民国采用CNS 7648的《资料元及交换格式–资讯交换–日期及时间的表示法》(与ISO 8601类似)称之为世界协调时间。中华人民共和国采用ISO 8601:2000的国家标准GB/T 7408-2005《数据元和交换格式 信息交换 日期和时间表示法》中亦称之为协调世界时。
协调世界时是世界上调节时钟和时间的主要时间标准,它与0度经线的平太阳时相差不超过1秒[4],并不遵守夏令时。协调世界时是最接近格林威治标准时间(GMT)的几个替代时间系统之一。对于大多数用途来说,UTC时间被认为能与GMT时间互换,但GMT时间已不再被科学界所确定。 ISO 8601 计算某一天在一年的第几周/循环时间RRlue/会用到此标准
国际标准ISO 8601,是国际标准化组织的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》。目前是第三版“ISO8601:2004”以替代第一版“ISO8601:1988”与第二版“ISO8601:2000”。
- UNIX时间
UNIX时间,或称POSIX时间是UNIX或类UNIX系统使用的时间表示方式:从协调世界时1970年1月1日0时0分0秒起至现在的总秒数,不考虑闰秒[1]。 在多数Unix系统上Unix时间可以通过date +%s指令来检查。
- 时区
时区列表