前言
此前一直觉得时区就是个小问题,并没有特意去注意过。直到在最近的工作中,反而因为时区的问题搞得我迷迷糊糊的。几番碰壁后,决定亲自探究下程序中的时间类型在存储、读取与比较中到底具有怎样的行为。本文以GO语言和mysql作为示例,不熟悉GO的朋友可以直接跳至结论,思考下在自己熟悉的语言中是否表现相同。
准备工作
一切从简,直接创建一个极简单的表,并插入一条记录,接下来就研究这个updated_at字段:
-- 建表
CREATE TABLE time_test (
id INT NOT NULL AUTO_INCREMENT,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- 插入一条晚上11点的数据
INSERT INTO time_test(updatedAt) VALUES("2021-01-22 23:00:00");
从数据库读取时间
现在我们数据库中有了一条记录,那么在程序语言中直接读入这个时间字段会怎样呢? 是2021-01-22 23:00:00吗?
运行如下程序,观察结果:
本文所有实例程序为了简便,错误处理从简,全扔给它👉_
package main
import (
"database/sql"
"fmt"
"time"
// driver
_ "github.com/go-sql-driver/mysql"
)
var localMysqlDSN = "root:rootroot@tcp(127.0.0.1:3306)/hello?parseTime=true"
func main() {
db, _ := sql.Open("mysql", localMysqlDSN)
defer db.Close()
var (
id int64
updatedAt time.Time
)
rows, _ := db.Query("SELECT * FROM time_test")
for rows.Next(){
_ = rows.Scan(&id, &updatedAt)
fmt.Printf("id: %v updatedAt:%v\n", id, updatedAt)
}
timeNow := time.Now()
fmt.Printf("timeNow: %v\n", timeNow)
}
从图中可以看到,从数据库中读取出来后是2021-01-22 23:00:00没错,但是,后面有个时区UTC,这是世界协调时间零时区的时间!而我们直接打印的当前时间是CST时区,即中国标准时间(China Standard Time),CST时间是经过UTC时间+8个小时后的时间。所以现在(我运行程序的时候)记录中的数据2021-01-22 23:00:00还是个未来时间。
瞅一眼,打印出的现在的时间timeNow, 跟电脑上的时间一致。所以程序中时间,默认会以电脑也就是你的系统时区为默认时区。为了验证这一点,修改电脑的时区为太平洋时间(美国和加拿大), 然后运行程序:
可以看到打印出的当前时间变了,比UTC时间少8个小时。证明程序中的时间默认会以操作系统中的时区为默认时区, 某些时间操作除外(见后文)。
所以,用docker时在这方面踩过坑的请举爪。
向数据库写入时间
在程序中将当前时间写入数据库,然后又读取出来:
package main
import (
"database/sql"
"fmt"
"time"
// driver
_ "github.com/go-sql-driver/mysql"
)
var localMysqlDSN = "root:rootroot@tcp(127.0.0.1:3306)/hello?parseTime=true"
func main() {
db, _ := sql.Open("mysql", localMysqlDSN)
defer db.Close()
timeNow := time.Now()
fmt.Printf("timeNow: %v\n", timeNow)
db.Exec("INSERT INTO time_test(updated_at) VALUES(?)",timeNow)
var (
id int64
updatedAt time.Time
)
rows, _ := db.Query("SELECT * FROM time_test")
for rows.Next(){
_ = rows.Scan(&id, &updatedAt)
fmt.Printf("id: %v updatedAt:%v\n", id, updatedAt)
}
}
可以看出,打印的当前时间为符合我们直觉的CST时间,存入数据库后mysql会自动将其转为UTC时间进行存储。当我们再次读取出来时,读取的也是减了8个小时的UTC时间。
向数据库写入时区时间
上面的过程中,程序使用了默认的系统时区,现在我们让当前时间主动加载不同的时区,然后分别存入数据库,是否存储的值是一样的呢?
package main
import (
"database/sql"
"fmt"
"time"
// driver
_ "github.com/go-sql-driver/mysql"
)
var localMysqlDSN = "root:rootroot@tcp(127.0.0.1:3306)/hello?parseTime=true"
func main() {
db, _ := sql.Open("mysql", localMysqlDSN)
defer db.Close()
cstSh, _ := time.LoadLocation("Asia/Shanghai")
usNY, _ := time.LoadLocation("America/New_York")
timeNowCN := time.Now().In(cstSh)
timeNowNY := time.Now().In(usNY)
fmt.Printf("timeNowCN: %v\n", timeNowCN)
fmt.Printf("timeNowNY: %v\n", timeNowNY)
db.Exec("INSERT INTO time_test(updated_at) VALUES(?),(?);",timeNowCN, timeNowNY)
var (
id int64
updatedAt time.Time
)
rows, _ := db.Query("SELECT * FROM time_test")
for rows.Next(){
_ = rows.Scan(&id, &updatedAt)
fmt.Printf("id: %v updatedAt:%v\n", id, updatedAt)
}
}
图中可以看出,尽管America/New_York时区要比UTC时间少5个小时,但无论是上海时区还是其他时区的时间,在存入mysql时,都自动转换为了UTC时间。所以同一时间点不同时区的 时间在存入数据库时,存储值都是一样的UTC时间。
获取某个时间当日零点
记得准备工作中插入的记录2021-01-22 23:00:00吗,这是有意安排的一个特殊时间,因为一旦处理不当掉入坑里,你就会把它处理为今天(2021年1月22日) !先运行如下程序:
package main
import (
"database/sql"
"fmt"
"time"
// driver
_ "github.com/go-sql-driver/mysql"
)
var localMysqlDSN = "root:rootroot@tcp(127.0.0.1:3306)/hello?parseTime=true"
func main() {
db, _ := sql.Open("mysql", localMysqlDSN)
defer db.Close()
var (
id int64
updatedAt time.Time
)
// 读取数据库第一行: updateAt = 2021-01-22 23:00:00 UTC
row := db.QueryRow("SELECT * FROM time_test")
_ = row.Scan(&id, &updatedAt)
fmt.Printf("id: %v updatedAt:%v\n", id, updatedAt)
cstSh, _ := time.LoadLocation("Asia/Shanghai")
updatedAtStr := updatedAt.Format("2006-01-02") // 2021-01-22
updatedAtStrSh := updatedAt.In(cstSh).Format("2006-01-02") // 2021-01-23
updatedDate1, _ := time.Parse("2006-01-02", updatedAtStr) // 2021-01-22 00:00:00 UTC
updatedDate2, _ := time.Parse("2006-01-02", updatedAtStrSh) // 2021-01-23 00:00:00 UTC
updatedDateSh1, _ := time.ParseInLocation("2006-01-02", updatedAtStr, cstSh) // 2021-01-22 00:00:00 CST 上海
updatedDateSh2, _ := time.ParseInLocation("2006-01-02", updatedAtStrSh, cstSh) // 2021-01-23 00:00:00 CST 上海
fmt.Println("updatedDate1:", updatedDate1)
fmt.Println("updatedDate2:", updatedDate2)
fmt.Println("updatedDateSh1:", updatedDateSh1)
fmt.Println("updatedDateSh2:", updatedDateSh2)
}
这里的逻辑是一个2x2的问题,即 格式化时间为字符串时以及解析时间为字符串时 分别加载时区与否。为了便于一眼就看明白,列个表格:
| 格式化时间为字符串 | 解析字符串为时间 | 结果 |
|---|---|---|
| 不带时区 | 不带时区 | 2021-01-22 00:00:00 UTC |
| 带时区 | 不带时区 | 2021-01-23 00:00:00 UTC |
| 不带时区 | 带时区 | 2021-01-22 00:00:00 CST |
| 带时区 | 带时区 | 2021-01-23 00:00:00 CST ✔ |
这里的不带时区是指不主动加载时区,其实默认的是UTC时区。
首先分析下正确的结果,2021-01-22 23:00:00在数据库中是UTC时间,要计算当日零点时间,以UTC时区来看,很明显就是当天2021-01-22 00:00:00。但是在中国,我们的项目运行基本都要求北京时间,所以,首先要转化为中国标准时间CST,加8个小时就是2021-01-23 07:00:00 CST,再计算其当日零点应该为2021-01-23 00:00:00 CST。所以在go程序中格式化、解析时间时都要加载时区。同时可以看出go程序的如下行为:
Format()视时区而定,传哪个时区的时间值,就格式化为哪个时区的字符串;Parse()解析字符串时间时默认UTC时区,与系统时区无关;
时间比较
前面分别讨论了时间在读写、格式化、解析时时区的表现。那么时间在进行比较时,会受时区影响吗?需要将比较的时间转换到同一个时区吗?
现在依然从数据库中获取2021-01-22 23:00:00这条记录(注意是UTC时间),然后分别获得其不同时区下的今日零点时间来进行比较:
package main
import (
"database/sql"
"fmt"
"time"
// driver
_ "github.com/go-sql-driver/mysql"
)
var localMysqlDSN = "root:rootroot@tcp(127.0.0.1:3306)/hello?parseTime=true"
func main() {
db, _ := sql.Open("mysql", localMysqlDSN)
defer db.Close()
var (
id int64
updatedAt time.Time
)
row := db.QueryRow("SELECT * FROM time_test")
_ = row.Scan(&id, &updatedAt)
fmt.Printf("id: %v updatedAt:%v\n", id, updatedAt)
cstSh, _ := time.LoadLocation("Asia/Shanghai")
usNY, _ := time.LoadLocation("America/New_York")
updatedAtCN :=updatedAt.In(cstSh)
updatedAtNY :=updatedAt.In(usNY)
fmt.Printf("updatedAtCN: %v\n", updatedAtCN)
fmt.Printf("updatedAtNY: %v\n", updatedAtNY)
// 两个时区今日0点
todayDateStrCN := updatedAtCN.Format("2006-01-02")
todayDateStrNY := updatedAtNY.Format("2006-01-02")
todayZeroTimeCN, _ := time.ParseInLocation("2006-01-02", todayDateStrCN, cstSh) //上海时区
todayZeroTimeNY, _ := time.ParseInLocation("2006-01-02", todayDateStrNY, usNY) // 纽约时区
fmt.Println("todayZeroTimeCN:", todayZeroTimeCN)
fmt.Println("todayZeroTimeNY:", todayZeroTimeNY)
fmt.Println("updatedAt is before China-today :", updatedAt.Before(todayZeroTimeCN))
fmt.Println("updatedAt is before NewYork-today:", updatedAt.Before(todayZeroTimeNY))
fmt.Println("updatedAt is after China-today :", updatedAt.After(todayZeroTimeCN))
fmt.Println("updatedAt is after NewYork-today :", updatedAt.After(todayZeroTimeNY))
}
updatedAt在不同时区下,一个为1月23日,一个为1月22日,但肯定都要晚于当日零点。从比较结果可以看出,在不同时区下进行比较都成立,说明程序中时间的比较是时区无关的(其实比较的是时间戳)。所以不同时区的时间可以直接进行比较。
结论
时间的读写
这里结论的前提是我们并没有对数据库进行任何时区相关的设置,比如mongo有时区感应tz_aware配置。在此前提下,可以得出:
- 除特定操作外,程序中的时间默认为操作系统时区,典型的如
time.Now()。 - 无论哪个时区的时间写入数据库时,都会转换为相应的UTC时间;
- 时间在读取时,存入的是UTC时间,读取的就是UTC时间;
时间的格式化与解析
- 时间在格式化时随时区而定,传递的是哪个时区的时间值,就格式化为哪个时区的字符串;
- 时间字符串在解析时,默认为UTC时区,与系统时区无关;
- 所以时间在格式化时、解析时都要加载时区;
时间的比较
- 时间的比较与时区无关,不同时区的时间可以直接进行比较,实际比较的是时间戳。
思考—判断两个日期是否是同一天
下面这段代码,是我在此前的工作中写的。假设系统的时间已经设置为了Asia/Shanghai时区,你觉得这个处理对吗?
// IsToday 判断统计日期是否是今天
// statTIme 是从数据库获取然后作为参数传递
var cstSh, _ = time.LoadLocation("Asia/Shanghai") //加载时区
func IsToday(statTime time.Time) bool {
statTimeStr := statTime.Format("2006-01-02")
todayTimeStr := time.Now().Format("2006-01-02")
statDate, err := time.ParseInLocation("2006-01-02", statTimeStr, cstSh)
if err != nil {
log.Fatalln(err)
}
todayDate, err := time.ParseInLocation("2006-01-02", todayTimeStr, cstSh)
if err != nil {
log.Fatalln(err)
}
if statDate.Equal(todayDate) {
return true
}
return false
}