深入理解Go時間處理(time.Time)

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有指定顯示時區, 那麼時區信息會體現在格式化後的時間字符串中.
    • 如果$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) {
.....
}

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零點即北京時間八點才更新.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章