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的行为

3.1、构造时间

1、 获取现在时间time.Now(),

  • time.Now()使用本地时间,time.Local即本地时区,取决于运行的系统环境配置,优先取”TZ”这个环境变量,然后取/etc/localtime,都取不到就用UTC兜底。
func Now() Time {
	sec, nsec := now()
	return Time{sec + unixToInternal, nsec, Local}
}

3、获取某一时区的现在时间-time.Now().In(),

  • Time结构体的In()方法仅设置loc, 不会改变时间值.
  • 特别地, 如果是获取现在的UTC时间, 可以使用Time.Now().UTC().
  • 时区不能为nil. time包中只有两个时区变量time.Local和time.UTC. 其他时区变量有两种方法取得,
    • 一个是通过time.LoadLocation函数根据时区名字加载, 时区名字见https://www.iana.org/time-zones。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)
}

3、手动构造时间-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")
	}
.....
}

4、 从unix时间戳中构造时间, time.Unix(), 传入秒和纳秒构造.

3.2、序列化反序列化时间

1、 文本和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
}

2、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"

3、序列化

time.Format("$layout")time.TimeYear()Month()Day()$layout$layouttime.Parse("$layout","$value"),$layouttime.Time$layoutUTCUTCtime.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) {
.....
}

4、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时区处理不当案例

1、有个服务频繁使用最新汇率, 所以缓存了最新汇率对象, 汇率对象的过期时间设为第二天北京时间零点, 汇率过期则从数据库中去最新汇率, 设置过期时间的代码如下:

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零点即北京时间八点才更新.