Go 学习笔记:如何在 Go 中实现类似 JavaScript setTimeout / setInterval 的功能

615 阅读3分钟

在 Go 中,可以使用 time 包来实现类似于 JavaScript 中的 setTimeout 和 setInterval 的功能。time 包提供了一个 Timer 结构体,用于在指定时间后调用一次函数,以及一个 Ticker 结构体,用于每隔指定时间调用一次函数。

setTimeout 实现

要实现 setTimeout 的功能,可以使用 time.AfterFunc 函数。该函数接受一个时间间隔和一个函数作为参数,并在指定时间间隔后调用该函数。以下是一个例子:

package main

import (
   "fmt"
   "time"
)

func setTimeout(callback func(), delay time.Duration) *time.Timer {
   return time.AfterFunc(delay, callback)
}

func main() {
   t1 := setTimeout(func() {
      fmt.Println("3 seconds have passed")
   }, 3*time.Second)

   t2 := setTimeout(func() {
      fmt.Println("10 seconds have passed")
   }, 10*time.Second)

   time.Sleep(5 * time.Second)
   t1.Stop()
   t2.Stop()
   time.Sleep(10 * time.Second)
}

在这段代码中,我们定义了一个 setTimeout 函数,它接受一个回调函数和一个时间间隔作为参数。在函数体内,我们使用 time.AfterFunc 函数来设置定时器,当时间到达时,就会执行回调函数。

setInterval 实现

要实现 setInterval 的功能,可以使用 time.Tick 函数和一个 for 循环。time.Tick 函数接受一个时间间隔作为参数,并返回一个 channel。通过在 for 循环中遍历该 channel 可以每隔指定时间调用一次函数。以下是一个例子:

package main

import (
   "fmt"
   "time"
)

func myFunc() {
   fmt.Println("Hello, world!")
}

func main() {
   ticker := time.Tick(1 * time.Second)
   for range ticker {
      myFunc()
   }
}

上面的代码会每隔 1 秒调用 myFunc 函数,并输出 "Hello, world!"。

但上述代码还存在一个问题:for 循环会阻塞 main 主进程执行,要解决这个问题,可以在另一个 goroutine 中运行 for 循环,以便将控制权还给主函数。以下是修改后的代码:

package main

import (
   "fmt"
   "time"
)

func myFunc() {
   fmt.Println("Hello, world!")
}

func main() {
   ticker := time.Tick(1 * time.Second)

   go func() {
      for range ticker {
         myFunc()
      }
   }()

   fmt.Println("主逻辑继续执行 --->")

   _, err := fmt.Scanln() // 防止程序立即退出
   if err != nil {
      return
   }
}

该 goroutine 将在程序运行期间一直运行,直到主函数接收到输入时退出。

如果要实现一个完整的 setInterval 函数功能,可以这么实现:

type IntervalHandler struct {
   ticker *time.Ticker
   stop   chan bool
}

func setInterval(callback func(), interval time.Duration) *IntervalHandler {
   ticker := time.NewTicker(interval)
   stop := make(chan bool)

   go func() {
      for {
         select {
         case <-ticker.C:
            callback()
         case <-stop:
            ticker.Stop()
            return
         }
      }
   }()

   return &IntervalHandler{
      ticker,
      stop,
   }
}

func (h *IntervalHandler) Stop() {
   h.stop <- true
}

在这个示例中,我们定义了一个 IntervalHandler 结构体,其中包含定时器的 Tickerstop 停止信号。在 setInterval 函数中,我们创建了一个定时器,并启动了一个协程,该协程会反复执行回调函数,直到接收到停止信号为止。最后,我们将 IntervalHandle 结构体的指针作为句柄返回。

使用 IntervalHandler 结构体的 Stop 方法可以停止定时器的循环执行,如下所示:

handler := setInterval(func() {
   fmt.Println("Tick!")
}, 1*time.Second)

time.Sleep(10 * time.Second)

handler.Stop()

在这个示例中,我们首先调用 setInterval 函数创建一个定时器,并将返回的句柄保存在 handler 变量中。然后,我们等待 10 秒钟,最后调用 handler.Stop() 方法停止定时器的循环执行。注意,停止定时器的操作必须在 setInterval 函数的协程中执行,否则会引发 panic 错误。