数据库的日期和时间
日期和时间是任何语言都必须处理好的事情。我们知道,在MySQL中,日期部分有date类型,时间部分有time类型,日期和时间都包含的类型有datetime类型(时间戳有timestamp类型)。比如,“2020-05-18 08:53:34.083”的日期部分就是“2020-05-18”,时间部分就是“08:53:34.083”。
语言对时间的诉求
在任何语言的使用过程中,我们无非是以下几点诉求:
- 时间能按照我们想要的格式化输出
- 时间能从 字符串 被解析成语言的内置时间类型
- 时间能加减、互相比较
- 能轻松计算一段代码的运行时间
- 能与数据库时间轻松地相互转换
- 支持时区设置,能轻松转换成某一地区的时间
Go语言中的时间能满足这上面的所有需求。
Go语言内置时间库
Go语言中的时间类型在内置的time库中,为Time类型。看一下Time的定义,wall是秒数,ext是纳秒,loc表示时区。这几个属性是私有属性,我们不必太关注。
type Time struct {
wall uint64
ext int64
loc *Location
}
新建Time实例可以使用time.Parse和time.ParseInLocation方法,或者使用time.Now()获取当前机器的时间。
还有Duration类型,表示两个时间之间的间隔,精确到纳秒,最大能表示大约290年的时间间隔。
type Duration int64
新建Duration实例可以使用time.ParseDuration方法或者直接获取Duration常量比如time.Second或者time.Minute等。
还有Timer类型,表示计时器。Timer中含有通道C,如果计时器不是使用AfterFunc方法创建的,则,当计时器到时间了,当前时间就会被传到通道C中;如果计时器是AfterFunc创建的,则计时器到时间了,会调用AfterFunc传入的f那个函数。
// The Timer type represents a single event.
// When the Timer expires, the current time will be sent on C,
// unless the Timer was created by AfterFunc.
// A Timer must be created with NewTimer or AfterFunc.
type Timer struct {
C <-chan Time
r runtimeTimer
}
上面提到语言对时间的诉求,我们来看Go语言如何满足我们的诉求。
第一条:时间能按照我们想要的格式化输出
在 Java 里,格式化输出都是使用“yyyy-MM-dd HH:mm:ss”,但是得知道这格式化字符串里面什么时候大写什么时候小写。Go语言可以很方便的按照我们的要求格式化输出日期,但是需要传入一个很奇怪的格式化字符串“2006-01-02 15:04:05”。看一下格式化当前时间:
curTime := time.Now() // 获取当前时间
fmt.Println(curTime) // 2020-05-19 10:32:07.1855433 +0800 CST m=+0.005985301
fmt.Println(curTime.Format("2006-01-02 15:04:05.000")) // 2020-05-19 10:32:07.185
fmt.Println(curTime.Unix()) // 获取时间戳,精确到秒
网上有人说这个时间是Go语言诞生时间,其实不是,这只是方便Go语言处理罢了。不过,这很好记忆,“2006-01-02 15:04:05”我们记作“六一二三四五”就好了。time.Unix()方法获取时间戳,精确到秒。
第二条:时间能从字符串被解析成语言的内置时间类型
在Java里,同样以格式化字符串“yyyy-MM-dd HH:mm:ss”可以把字符串表示的时间解析成java.util.Date类型。在Go语言里同理,还是使用“2006-01-02 15:04:05”把时间解析成Go内置的Time类型:
t, _ := time.Parse("2006-01-02 15:04:05.000", "2020-05-19 10:32:07.185") // 将会使用默认时区+0000进行解析
fmt.Printf("%T", t) // time.Time
%T表示输出变量的数据类型。可以看到,“2020-05-19 10:32:07.185”成功的被解析成了time.Time类型。
第三条:时间能加减、互相比较
时间互相比较,After和Before可以比较“时间先后”,Equal比较“相等”:
t1, _ := time.ParseInLocation("2006-01-02 15:04:05.000", "2020-05-19 10:32:07.185", time.Local)
t2 := time.Now()
fmt.Printf("t1:%v\n", t1) // t1:2020-05-19 10:32:07.185 +0800 CST
fmt.Printf("t2:%v\n", t2) // t2:2020-05-19 12:07:34.9199443 +0800 CST m=+0.027924401
fmt.Println("t1 before t2: ", t1.Before(t2)) // t1 before t2: true
fmt.Println("t1 after t2: ", t1.After(t2)) // t1 after t2: false
fmt.Println("t1 equal t2: ", t1.Equal(t2)) // t1 equal t2: false
时间加减,在时间Time实例上使用Add传入时间间隔Duration,返回新的时间Time实例。从本文最开始处我们知道,Duration是int64类型,因此可以直接在Duration使用负号表示往前的时间间隔:
t1, _ := time.ParseInLocation("2006-01-02 15:04:05.000", "2020-05-19 10:32:07.185", time.Local)
fmt.Println(t1) // 2020-05-19 10:32:07.185 +0800 CST
oneDay, _ := time.ParseDuration("24h")
t3 := t1.Add(oneDay) // 往后24小时
t4 := t1.AddDate(0, 0, 1) // 往后1天
fmt.Println(t3) // 2020-05-20 10:32:07.185 +0800 CST
fmt.Println(t4) // 2020-05-20 10:32:07.185 +0800 CST
t5 := t1.Add(-oneDay) // 往前1天
fmt.Println(t5) // 2020-05-18 10:32:07.185 +0800 CST
d := t1.Sub(t5)
fmt.Println(d) // 24h0m0s
如上面这个例子,还可以使用AddDate更方便的加减年月日的时间。另外,两个Time实例使用Sub相减能得到代表着这两个时间差的Duration实例,见上面Sub的使用案例。
第四条:能轻松计算一段代码的运行时间
在Java里,我们要计算代码运行时间,一般会在开始处使用System.currentTimeMillis()获取当前时间,在结尾处再次获取,两个时间相减得到中间这段代码的运行时间。在Go语言中有更方便的处理方式:
startTime := time.Now()
for i:=0;i<200000;i++ {
fmt.Println(i)
}
duration := time.Since(startTime)
fmt.Println(duration) // 输出:554.5179ms
time.Since传入一个起始时间作为参数,则会返回当前时间和这个起始时间的间隔Duration实例。上面这个例子中循环运行了554.5179毫秒。
第五条:能与数据库时间轻松地相互转换
Go语言连接MySQL有比较著名的开源库go-sql-driver,支持MySQL, MariaDB, Google CloudSQL等等数据库,Github就可以搜到。在Go语言中,MySQL中date和datetime类型的默认输出值是[]byte,这是为了方便自己的代码将时间值转换为字节切片、字符串或者sql.RawBytes类型进行处理。要想让date和datetime类型直接转换为Go语言的time.Time类型,只需要在连接字符串上加上parseTime=true即可。
db, err := sql.Open("mysql", "user:password@/dbname?charset=utf8mb4&parseTime=true")
我们还可以在连接字符串上指定loc或者time_zone来指定时区。
第六条:支持时区设置,能轻松转换成某一地区的时间
上一条提到了,可以在连接字符串上指定时区,来决定时间值存入数据库的时区。在Go语言中,Time类型自带时区属性,可以很方便的切换时区输出。Go语言默认时区是UTC时区,time.Parse方法解析不带时区的时间会自动使用UTC时区。北京时间是东八区时间,在时间字符串可以使用CST或者+0800来表示东八区时间。看下面的例子:
timeUTC, _ := time.Parse("2006-01-02 15:04:05.000", "2020-05-19 10:32:07.185")
fmt.Println(timeUTC) // 2020-05-19 10:32:07.185 +0000 UTC
timeCST1, _ := time.Parse("2006-01-02 15:04:05.000 MST", "2020-05-19 10:32:07.185 CST")
fmt.Println(timeCST1) // 2020-05-19 10:32:07.185 +0800 CST
timeCST2, _ := time.Parse("2006-01-02 15:04:05.000 -0700", "2020-05-19 10:32:07.185 +0800")
fmt.Println(timeCST2) // 2020-05-19 10:32:07.185 +0800 CST
fmt.Println(timeCST1 == timeCST2) // true
Go语言切换时区可以使用time.In方法,首先获取一个Location实例,然后转换即可:
timeCST, _ := time.Parse("2006-01-02 15:04:05.000 MST", "2020-05-19 10:32:07.185 CST")
fmt.Println(timeCST) // 2020-05-19 10:32:07.185 +0800 CST
location, _ := time.LoadLocation("America/New_York")
newYorkTime1 := timeCST.In(location)
fmt.Println(newYorkTime1) // 2020-05-18 22:32:07.185 -0400 EDT
location2 := time.FixedZone("EDT", -4*3600)
newYorkTime2 := timeCST.In(location2)
fmt.Println(newYorkTime2) // 2020-05-18 22:32:07.185 -0400 EDT
我们可以使用time.LoadLocation和time.FixedZone来获取Location实例。LoadLocation是通过IANA标准的时区字符串来获取时区。FixedZone是通过相对UTC时间的偏移量来获取时间的,第一个参数是时区字符串,第二个参数是相对UTC偏移的秒数。
其他黑魔法:超时控制
Go语言time提供了两个很重要的方法来实现超时控制:time.After和time.AfterFunc。
time.After传入一个Duration实例参数,返回一个单向的时间通道,该方法不会阻塞。在过了Duration时间后,那个时刻的时间将会传入通道中返回(通道是无缓冲通道)。这样我们可以很方便的控制我们自己的代码是否需要阻塞等待或者在Duration时间内做其他的事情。
import (
"time"
)
func main () {
ch := time.After(time.Second*10)
<-ch // 此处阻塞10秒,10秒后程序结束
}
time.AfterFunc传入Duration和一个函数作为参数,同样不会阻塞当前代码。在Duration时间之后,将会执行我们传入的函数。
import (
"fmt"
"time"
)
func main() {
time.AfterFunc(time.Second*10, func() {
fmt.Println("10 seconds later.")
})
time.Sleep(time.Second * 15)
}
程序将在10秒后输出“10 seconds later.”,15秒后程序退出。
time.After和time.AfterFunc其实都是使用Timer来实现的。我们也可以直接使用Timer来实现:
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(time.Second * 10)
<- timer.C
fmt.Println("10 seconds later.")
}
同样在10秒后输出“10 seconds later.”。