Golang语法进阶(定时器)

文章目录

在我们项目中,常常会有这样的场景,比如 到了未来某一时刻,需要某个逻辑或者某个任务执行一次,或者是周期性的的执行多次 ,有点类似定时任务。这种场景就需要用到定时器,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

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
hweiyu002 小时前
强连通分量算法:Kosaraju算法
算法·深度优先
期待のcode2 小时前
TransactionManager
java·开发语言·spring boot
郝学胜-神的一滴2 小时前
Linux系统编程:深入理解读写锁的原理与应用
linux·服务器·开发语言·c++·程序人生
Larry_Yanan2 小时前
Qt多进程(十一)Linux下socket通信
linux·开发语言·c++·qt
计算机学姐2 小时前
基于SpringBoot的汽车租赁系统【个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·后端·spring·汽车·推荐算法
mit6.8242 小时前
逆向思维|memo
算法
好好研究2 小时前
SpringBoot小案例打包执行流程
java·spring boot·后端
机器学习之心2 小时前
MATLAB灰狼优化算法(GWO)改进物理信息神经网络(PINN)光伏功率预测
神经网络·算法·matlab·物理信息神经网络
BingoGo2 小时前
免费可商用商业级管理后台 CatchAdmin V5 正式发布 插件化与开发效率的全面提升
vue.js·后端·php