今天学习time

  • time.Time
  • time.Duration
  • time.Now()和time.Since()
  • Add
  • AddDate
  • time.Date
  • 日期格式化
  • time.Parse
  • Unix时间戳
  • 时间比较

time.Time

time.Time表示某个具体瞬间,精度到纳秒,通常建议把time.Time当值传递,而不是用*time.Time;它的零值是0001-01-01 00:00:00 UTC,判断是否初始化应该使用.IsZero()

从他的零值就可以看到,它有别于时间戳,它的默认值是公元1年1月1日。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type User struct {
ID int64
LastLogin time.Time
}

func NeedFirstLoginGuide(u User) bool {
return u.LastLogin.IsZero()
}

func TestTimeTime(t *testing.T) {
u := User{
ID: 1,
LastLogin: time.Now(),
}

t.Log(NeedFirstLoginGuide(u))
}

time.Duration

表示两个时间点之间的间隔,Duration底层是int64,单位是纳秒

1
2
3
4
5
6
7
8
type Config struct {
TimeoutSeconds int
}

func Timeout(cfg Config) time.Duration {
// 请不要写成 cfg.TimeoutSeconds * time.Second 单位不一样
return time.Duration(cfg.TimeoutSeconds) * time.Second
}

time.Second不是普通整数,而是time.Duration的类型常量。

time.Now()、time.Since()

time.Now()返回当前本地时间;从time.Now()得到的值包含wall clockmonotonic clock。Go在计算SubSinceUntil等耗时时,会优先使用单调时钟,避免系统时间被NTP或人工调整。

1
2
3
4
5
6
7
func TestTimeNowAndSince(t *testing.T) {
start := time.Now()
defer func() {
fmt.Println("cost :", time.Since(start))
}()
time.Sleep(120 * time.Millisecond)
}

测量耗时用time.Since(start),不要用time.Now().Unix() - start.Unix()

Add:加固定时长

Add(d)表示在某个时间点上加一个固定的Duration

1
2
3
func NewCodeExpireAt() time.Time {
return time.Now().Add(5 * time.Minute)
}

Add(24 * time.Hour)是加固定24小时,不等于日历上的明天

AddDate: 加年、月、日

AddDate(years,months,days)用于日历意义上的加年、月、日。

1
2
3
4
5
6
7
func TestAddDate(t *testing.T) {
// Go的时间格式化必须使用这个特定的时间:2006-01-02 15:04:05
layout := "2006-01-02 15:04:05"
now := time.Now()
fmt.Println(now.Format(layout))
fmt.Println(NextBillingDate(now).Format(layout))
}

AddDate(0,1,0)Add(30 * 24 * time.Hour)是不一样的,切记切记。会受到夏令时、月底等因素造成结果不同。

time.Date 构造时间

time.Date可以构造指定年月日时分秒纳秒和时区的事件。他会对超出范围的月、日、时、分、秒、做归一化。例如10月32日会变成11月1日。传入nillocation会panic

1
2
3
4
5
6
7
8
func LastDayOfMonth(year int, month time.Month, loc *time.Location) time.Time {
return time.Date(year, month+1, 0, 0, 0, 0, 0, loc)
}

func TestTimeDate(t *testing.T) {
layout := "2006-01-02 15:04:05"
t.Log(LastDayOfMonth(2026, time.June, time.Local).Format(layout))
}

格式化

Go的时间格式化并不是yyyy-MM-dd,而是使用固定参考时间2006-01-02 15:04:05 (记忆:01-02 下午 03:04:05 06年)

1
2
3
4
5
func TestTimeDate(t *testing.T) {
layout := "2006-01-02 15:04:05"
now := time.Now()
t.Log(now.Format(layout))
}

如果不想用2006-01-02 15:04:05,你可以使用任意一个时间,因为它是参考格式,并不是内容:2026-06-19 10:00:00

time.Parse 解析

time.Parse(layout,value)使用layout解析字符串;如果字符串没有时区信息,Parse默认返回UTC时间。

1
2
3
4
5
6
7
8
9
10
11
func ParseUTC(s string) (time.Time, error) {
return time.Parse("2006-01-02 15:04:05", s)
}

func TestTimeParse(t *testing.T) {
currentTime, err := ParseUTC("2026-06-18 15:00:00")
if err != nil {
t.Fatal(err)
}
t.Log(currentTime.AddDate(0, 0, 1).Format("2006-01-02 15:04:05"))
}

time.ParseInLocation本地时区解析

用户输入2026-06-18 09:00:00通常表示用户所在时区的9点,这是应使用ParseInLocation

1
2
3
4
5
6
7
8
9
10
11
func ParseShanghaiTime(s string) (time.Time, error) {
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
return time.Time{}, err
}
return time.ParseInLocation("2006-01-02 15:04:05", s, loc)
}

func TestTimeParseInLocation(t *testing.T) {
t.Log(ParseShanghaiTime("2026-06-18 15:00:00"))
}

Unix时间戳

time.Unix(sec,nsec)用秒和纳秒构造时间,另外还提供UnixMilliUnixMicro等方法处理毫秒和微秒时间戳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func FromJSTimestamp(ms int64) time.Time {
return time.UnixMilli(ms)
}

func ToJSTimestamp(t time.Time) int64 {
return t.UnixMilli()
}

func TestUnix(t *testing.T) {
ms := ToJSTimestamp(time.Now())
t.Log(ms)

unixTime := FromJSTimestamp(ms)
t.Log(unixTime.Format("2006-01-02 15:04:05"))
}

通常前端使用的都是毫秒,而Go的Unix()是秒。不要把毫秒吴传给time.Unix(ms,0)

时间比较

Equal判断两个Time是否表示同一瞬间,即使它们处于不同时区也可以相等。 通常建议使用t.Equal(u)来判断,而不是t == u,因为==还会比较Location和单调时钟信息。其他的比较还有AfterBeforeCompare

1
2
3
4
5
6
7
8
9
10
11
// 是否过期
func IsExpired(expireAt time.Time) bool {
return time.Now().After(expireAt)
}
// 测试不同地区的时间是否是同一瞬间
func TestTimeEqual(t *testing.T) {
utc := time.Date(2026, time.June, 18, 15, 0, 0, 0, time.UTC)
shanghai, _ := time.LoadLocation("Asia/Shanghai")
cn := time.Date(2026, time.June, 18, 15, 0, 0, 0, shanghai)
t.Logf("UTC: %v, Shanghai: %v, Equals %v ", utc, cn, utc.Equal(cn))
}

Round和Truncate:时间取整

Time.Round(d)会四舍五入到最近的d倍数。Time.Truncate(d)会向下截断到d倍数。

1
2
3
4
5
6
7
func TestTimeRound(t *testing.T) {
now := time.Now()
// 2026-06-19 10:26:00
t.Log(now.Round(time.Minute).Format("2006-01-02 15:04:05"))
// 2026-06-19 10:25:00
t.Log(now.Truncate(time.Minute).Format("2006-01-02 15:04:05"))
}

Truncate(24 * time.Hour)是按照绝对时间阶段,不等于本地当天零点。

如果想要实现本地当天零点:

1
2
3
4
func TestTimeRound1(t *testing.T) {
y, m, d := time.Now().Date()
t.Log(time.Date(y, m, d, 0, 0, 0, 0, time.Local).Format("2006-01-02 15:04:05"))
}

time.Sleep:暂停当前goroutine

这个和Java的一模一样了,都是让当前线(协)程睡眠一会儿。

简单的重试

1
2
3
4
5
6
7
8
9
10
11
func Retry(fn func() error) error {
var err error
for i := 0; i < 3; i++ {
if err = fn(); err != nil {
return nil
}

time.Sleep(time.Duration(i+1) * time.Second)
}
return err
}

Time.Sleep() 只阻塞当前goroutine,不会阻止整个进程。

time.After: 一次性超时channel

time.After(d)返回一个channel,到期后会受到当前时间。未引用且停止的timer可以被GC回收;

select超时:

1
2
3
4
5
6
7
8
func ReadWithTimeout(ch <-chan string) (string, error) {
select {
case v := <-ch:
return v, nil
case <-time.After(2 * time.Second):
return "", fmt.Errorf("timeout")
}
}

循环中频繁创建time.After可能带来额外分配,对于这种高频循环,更推荐复用Timer

NewTimer\Stop\Reset:可控的一次性的定时器

Timer表示一次性事件,到期后会向c发送当前事件,从Go 1.23开始 timer channel默认是同步、无缓冲的。StopReset返回后不会在收到就得stale time

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 复用Timer做空闲超时
func WatchIdle(events <-chan struct{}) {
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
for {
select {
case <-events:
timer.Reset(10 * time.Second)
case <-timer.C:
fmt.Println("idel timeout")
return
}
}
}

AfterFunc:到期后启动goroutine执行函数

time.AfterFunc(d,f)会等待d后在自己的goroutine里执行f;Stop不会等待已久启动的f结束。

1
2
3
4
5
6
// 超时自动关闭资源
func AutoClose(closeFn func()) *time.Timer {
return time.AfterFunc(30*time.Second, func() {
closeFn()
})
}

AfterFunc的回调可能并发执行,如果Reset后旧调用还没结束,要自己用锁、Channel或者context协调。

Ticker:周期性任务

Ticker用于周期性出发,从Go 1.23开始,未引用的ticker可以被GC回收;Stop不在为了帮助GC必须,但业务上想停止周期任务任需要调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 每秒上报一次心跳
func HeartBeat(done <-chan struct{}) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("Ping ...")
case <-done:
return
}
}
}

获取年月日、星期、ISO周

Time提供Date()Clock()Weekday()ISOWeek()YearDay()等方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func TestTimeDate1(t *testing.T) {
date := time.Date(2026, 6, 10, 10, 30, 0, 0, time.Local)
// date: 2026-06-10 10:30:00 +0800 CST, year: 2026, month: 6, day: 10
t.Logf("date: %v, year: %d, month: %d, day: %d", date, date.Year(), date.Month(), date.Day())
// 2026 June 10
t.Log(date.Date())
// 10 30 0
t.Log(date.Clock())
// Wednesday
t.Log(date.Weekday())
// 161
t.Log(date.YearDay())
// 2026 24
t.Log(date.ISOWeek())
}