文章目录
在我们项目中,常常会有这样的场景,比如 到了未来某一时刻,需要某个逻辑或者某个任务执行一次,或者是周期性的的执行多次 ,有点类似定时任务。这种场景就需要用到定时器,golang中也内置了定时器的实现,timer和ticker。
Timer
Time是一种一次性时间定时器,即在未来某个时刻,触发的事件只会执行一次。
Timer的结构定义
go
type Timer struct {
C <-chan Time
initTimer bool
}
Timer结构里有一个Time类型的管道C,主要用于事件通知 。在未到达设定时间的时候,管道内没有数据写入,一直处于阻塞状态,到达设定时间后,会向管道内写入一个系统时间,触发事件
创建Timer
go
func NewTimer(d Duration) *Timer
用法示例:
go
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(2 * time.Second) //设置超时时间2s
// 用户代码是不能往只读channel写数据的
<-timer.C
fmt.Println("after 2s Time out!")
}
运行结果:
go
after 2s Time out!
程序在2s后打印" after 2s Time out!",因为创建了一个定时器timer,设置了超时时间为2s,执行<-timer.C会一直阻塞,直到2s后,程序继续执行。
停止Timer
go
func (t *Timer) Stop() bool
返回值:
- true:执行stop()时timer还没有到达超时时间,即超时时间内停止了timer。
- false:执行stop()时timer到达了超时时间,过了超时时间才停止timer。
用法示例:
go
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(2 * time.Second) //设置超时时间2s
res := timer.Stop()
fmt.Println(res)
}
运行结果:
go
true
重置Timer
go
func (t *Timer) Reset(d Duration) bool
Reset 将计时器更改为在持续时间 d 后过期。如果计时器处于活动状态,则返回 true;如果计时器已过期或已停止,则返回 false。
对于已经过期或者是已经停止的timer,可以通过重置方法激活使其继续生效
用法示例:
go
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(time.Second * 2)
<-timer.C
fmt.Println("time out1")
res1 := timer.Stop()
fmt.Printf("res1 is %t\n", res1)
timer.Reset(time.Second * 3)
res2 := timer.Stop()
fmt.Printf("res2 is %t\n", res2)
}
运行结果:
go
time out1
res1 is false
res2 is true
程序2s之后打印"time out1",此时timer已经过期了,所以res1的值为false ,接下来执行timer.Reset(time.Second * 3)又时timer生效了,并且重设超时时间为3s,但是紧接着执行了timer.Stop(),还未到超时时间,所以res2的值为true。
time.AfterFunc
方法定义:
go
func AfterFunc(d Duration, f func()) *Timer
time.AfterFunc参数为超时时间d和一个具体的函数f,返回一个Timer的指针 ,作用time.AfterFunc(d, f) 会创建一个定时器;到时间 d 之后,Go 会在一个新的 goroutine 中调用 f(不会阻塞/占用你调用 AfterFunc 的那个 goroutine);函数立刻返回 *time.Timer
go
package main
import (
"fmt"
"time"
)
func main() {
duration := time.Second
f := func() {
fmt.Println("f has been called after 1s by time.AfterFunc")
}
timer := time.AfterFunc(duration, f)
// defer timer.Stop():用于"保险"地取消定时器,防止不该执行的回调在未来触发
defer timer.Stop()
time.Sleep(2 * time.Second)
}
运行结果:
go
f has been called after 1s by time.AfterFunc
1s之后打印语句
time.After
方法定义
go
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
根据函数定义可以看到,After函数会返回timer里的管道,并且这个管道会在经过时段d之后写入数据 ,调用这个函数,就相当于实现了定时器。
一般time.After会配合select一起使用,使用示例如下:
go
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(time.Second * 3)
ch <- "test"
}()
select {
case val := <-ch:
fmt.Printf("val is %s\n", val)
case <-time.After(time.Second * 2):
fmt.Println("timeout!!!")
}
}
运行结果:
go
timeout!!!
程序创建了一个管道ch,并且在主goroutine用select监听两个管道,一个是刚刚创建的ch,一个是time.After函数返回的管道c,ch管道3s之后才会有数据写入,而time.After函数是2s超时 ,所以2s后就会有数据写入,这样select会先收到管道c里的数据,执行timeout退出。
Ticker
方法定义如下:
go
func NewTicker(d Duration) *Ticker
NewTicker用于返回一个Ticker对象
Ticker对象定义
go
type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
initTicker bool
}

Ticker对象的字段和Timer是一样的,也包含一个通道字段,并会每隔时间段 d 就向该通道发送当时的时间 ,根据这个管道消息来触发事件,但是ticker只要定义完成,就从当前时间开始计时,每隔固定时间都会触发,只有关闭Ticker对象才不会继续发送时间消息
用法示例:
go
package main
import (
"fmt"
"time"
)
func Watch() chan struct{} {
ticker := time.NewTicker(1 * time.Second)
ch := make(chan struct{})
go func(ticker *time.Ticker) {
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("watch!!!")
case <-ch:
fmt.Println("Ticker Stop!!!")
return
}
}
}(ticker)
return ch
}
func main() {
ch := Watch()
time.Sleep(5 * time.Second)
ch <- struct{}{}
close(ch)
}
运行结果:
go
watch!!!
watch!!!
watch!!!
watch!!!
watch!!!
Ticker Stop!!!
Watch函数里创建一个ticker,将它传递到子goroutine函数,每隔1s打印"watch!!!",主函数创建一个管道ch,通过ch来控制go func()函数的退出,在5s之后主函数发送一个信号到ch,watch函数select收到ch信号,将return,在return之前将执行defer ticker.Stop()语句关闭ticker。在这5s之间,select将每隔1s收到ticker.C管道里的消息,打印watch!!!"。
注意
请一定要记住在使用结束后呼叫Stop,否则会造成memory leak的问题,比如定时任务跑在后台的goroutine上一定要记住在结束前呼叫Stop()

go
package main
import (
"fmt"
"time"
)
func NewCronJob() *time.Ticker {
ticker := time.NewTicker(1 * time.Second)
go func(ticker *time.Ticker) {
for range ticker.C {
fmt.Println("Cron job...")
}
fmt.Println("Ticker Stop! Channel must be closed.")
}(ticker)
return ticker
}
func main() {
ticker := NewCronJob()
time.Sleep(5 * time.Second)
ticker.Stop()
}
go
root@GoLang:~/proj/goforjob# go run main.go
Cron job...
Cron job...
Cron job...
Cron job...
Cron job...
root@GoLang:~/proj/goforjob#
单从程式看 fmt.Println("Ticker Stop! Channel must be closed.") 这行会发生在 ticker.C 关闭后,实际执行结果并没有执行到这行,意味着其实 Ticker 内部的 Channel 是没有关闭的,如果今天程式不断的使用这种写法,最终会导致主机严重的 memory leak 发生。
如果没有看过实际的 source code,就不会发现其实在注解的时候已经有明确说明了,Stop 并不会关闭 channel,预防读取 channel 发生错误,所以在 Ticker 的使用上,比较推荐下方这种写法,可以有效的执行超时之后下方的程式码,实现起来也比较优雅。
go
package main
import (
"fmt"
"time"
)
func NewCronJob() chan bool {
ticker := time.NewTicker(1 * time.Second)
stopChan := make(chan bool)
go func(ticker *time.Ticker) {
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("Cron job...")
case <-stopChan:
{
fmt.Println("Ticker Stop! Channel must be closed")
return
}
}
}
}(ticker)
return stopChan
}
func main() {
stopController := NewCronJob()
time.Sleep(5 * time.Second)
close(stopController)
}
go
root@GoLang:~/proj/goforjob# go run main.go
Cron job...
Cron job...
Cron job...
Cron job...
Cron job...
Ticker Stop! Channel must be closed
root@GoLang:~/proj/goforjob#
Ticker.Stop() 不会关闭 ticker.C,所以 for range ticker.C 不能靠 Stop 结束,会一直阻塞等待下一次 tick(即使不会再有 tick)。
要实现可停止的定时循环,推荐用 select 监听一个退出信号(stopChan 或 context.Done()),并在退出前调用 ticker.Stop()。
退出 goroutine 的意义在于:不再持有 ticker 的引用,让相关对象变成不可达,才能被 GC 回收。
for range ticker.C 只有在 ticker.C 永远不关闭 的情况下才会一直阻塞等待下一个值。这种写法必须配合退出机制(比如 select 或额外信号),否则 goroutine 就无法退出。
GC 回收的是"不可达对象",不是"没有 goroutine 持有"。只要还有引用(变量、闭包、结构体字段等),就不会回收。
内置函数 close 用于关闭一个通道,该通道必须是双向通道或仅发送通道。它只能由发送方执行,接收方绝不能执行,其作用是在接收到最后一个发送值后关闭通道。从已关闭的通道 c 接收到最后一个值后,任何从 c 接收数据的操作都会成功,而不会阻塞,并返回通道元素的零值。对于已关闭且为空的通道,x, ok := <-c 这样的形式也会将 ok 设置为 false。
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!