0%

Go并发编程

Go 并发编程

并发基本概念

进程 Process 与线程 Thread

  • 进程是系统进行资源分配和调度的一个基本单位,程序在操作系统中的一次执行过程
  • 线程是进程的执行单位,是CPU调度和分派的基本单位
  • 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行

并行 Concurrent 与并发 Paralled

  • 并行:多个线程同时操作多个资源类

    并行

  • 并发:多个线程交替操作同一资源类

    并发

进程 process 与线程 thread 与协程 coroutine

  • 一个进程上可以跑多个线程,一个线程上可以跑多个协程
  • 多个线程可以利用多个 CPU 并行,但一个线程内的多个协程是串行的,同一时刻只能有一个在运行,无法利用 CPU 多核,但不同线程内的协程之间可以并行
进程 process 线程 thread 协程 coroutine
切换者 操作系统 操作系统 用户
切换内容 页全局目录、内核栈、硬件上下文 内核栈、硬件上下文 硬件上下文
切换内容保存位置 内存 内核栈 用户栈或堆(变量)
状态切换 用户态 —> 内核态 —> 用户态 用户态 —> 内核态 —> 用户态 用户态
切换效率
  • 进程、线程的切换者是操作系统,操作系统决定切换时刻,用户无感
  • 协程的切换者是用户,由用户程序决定切换时间
  • 进程切换内容:页全局目录、内核栈、硬件上下文,切换的内容保存在内存中,采用 用户态 —> 内核态 —> 用户态
  • 线程切换内容:内核栈、硬件上下文,切换的内容保存在内核栈中,采用 用户态 —> 内核态 —> 用户态
  • 协程切换内容:硬件上下文,切换的内容保存在用户栈或堆(变量)中,切换过程始终处于用户态

协程 Goroutine

Go 不需要自己编写进程、线程、协程,直接使用 goroutine,在语言中内置了调度和上下文切换机制,可轻松开启上万 goroutine。

goroutine 协程概念上类似于线程,Go 程序可以智能地将 goroutine 分配给不同的 CPU,由Go的运行时(runtime)调度和管理的。

使用 goroutine

goroutine 使用时将任务包装成函数,通过 go 关键字开启

1
go func()

如果 goroutine 没有执行完,但主线程已经结束,goroutine 也会跟着结束。

示例

每两秒输出一次 go routine,每秒输出一次 hello,输出 10 次 hello 后结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"time"
)

func runtime() {
for {
fmt.Println("go routine")
time.Sleep(time.Second * 2)
}
}

func main() {
go runtime()

for i := 0; i < 10; i++ {
fmt.Println("hello")
time.Sleep(time.Second)
}
}

协程调度

GPM 模型

goroutine 调度系统为 GPM 运行时(runtime)调度器,包括了 3 个部分—— goroutine G、处理器 P、线程 M

  • G:goroutine
    • 存放本 goroutine 的一些信息,以及与 P 绑定等信息
  • P:Processor,处理器
    • P 对 goroutine 队列进行调度
    • goroutine 与线程的中间层,管理着一组 goroutine 队列,储存所管 goroutine 运行的上下文
    • P 的数量决定了最大可并行数量,个数由 runtime.GOMAXPROCS 设置,最大 256,默认为 CPU 数
  • M:Machine,线程
    • Go 运行时对操作内核线程的虚拟,与内核线程一一对应

P 管理着一群 G,调度在 M 上运行。一般比例为 G : P : M = n : 1 : 1

GPM调度

调度机制:

  • 把占用 CPU 时间过长的 goroutine 暂停,去运行后续的 goroutine
  • 当自己队列的消费光了,则去取全局队列中的 goroutine
  • 如全局队列也消费光了,则去抢其他 P 的 goroutine
  • 如果一个 G 长时间占据着 M,runtime 就会新建一个 M,管理阻塞 G 的 P 会将其他的 G 都挂到新建的 M 上。当旧的 M 上的 G 运行完或者被判定为死掉时,就会回收旧有的 M。

GPM调度机制

由此可以看出,从调度角度看,goroutine 相较于线程的优势:

  • goroutine 的调度由 runtime 调度器调度,全程在用户态
  • 线程需要内核进行调度,需要内核态和用户态之间频繁切换

runtime 包

runtime 运行时用来调度和管理 goroutine,可通过 runtime 包程序控制 goroutine 以及获取环境信息。

runtime.Gosched()

释放当前 goroutine 的 CPU 时间片给其他 goroutine 执行,当前 goroutine 等待未来的时间片再执行。

示例

下面代码,运行后有两种可能:

  • 一段数字 + 两个 hello
    • CPU 时间片先给数字协程,再分配给 hello 主协程
  • 两个 hello
    • CPU 时间片先给 hello 主协程,主协程结束后程序直接结束,不输出数字,再分配数字协程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

func main() {
go func() {
for i := 0; i < 100000; i++ {
fmt.Println(i)
}
}()
// 主协程
for i := 0; i < 2; i++ {
fmt.Println("hello")
}
}

这时在主协程运行输出 hello 前,添加 runtime.Gosched(),会切到其他 goroutine 输出多次数字,等数字协程的时间片用完时,切回主协程输出 hello ,再切回数字协程输出数字,最后主协程输出 hello 后结束程序。

即输出:一段数字 + hello + 一段数字 + hello

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"runtime"
)

func main() {
go func() {
for i := 0; i < 100; i++ {
fmt.Println(i)
}
}()
// 主协程
for i := 0; i < 2; i++ {
// 切到其他进程先执行
runtime.Gosched()
fmt.Println("hello")
}
}

runtime.Goexit()

退出当前 goroutine,不过退出前还是会正常执行 defer 语句。

示例

下面代码,主协程休眠一秒,数字协程输出一段数字之后,主协程苏醒输出 hello

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"time"
)

func main() {
go func() {
for i := 0; i < 100; i++ {
// runtime.Goexit()
fmt.Println(i)
}
}()
time.Sleep(time.Second)
fmt.Println("hello")
}

添加 runtime.Goexit() 后,直接结束协程,不会输出数字,只输出 hello

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"runtime"
"time"
)

func main() {
go func() {
for i := 0; i < 100; i++ {
runtime.Goexit()
fmt.Println(i)
}
}()
time.Sleep(time.Second)
fmt.Println("hello")
}

runtime.GOMAXPROCS()

设置可同时使用最大 CPU 核数,并返回之前的设置。

1
func runtime.GOMAXPROCS(n int) int

其他运行时信息

  • 获取 CPU 核数量

    1
    runtime.NumCPU()
  • 获取 GOROOT 路径

    1
    runtime.GOROOT()
  • 获取操作系统

    1
    runtime.GOOS

数据共享

临界资源

临界资源:并发环境中多个进程/线程/协程共享的资源。

多个 goroutine 访问同一资源时,多个写 goroutine,会造成临界资源安全问题。

下面示例中,4 个售票员并发卖 10 张票,原本是希望卖到无票时所有售票员停止卖票,但同时读写就可能导致资源安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"math/rand"
"time"
)

var numTicket = 10

func saleTicket(i int) {
rand.Seed(time.Now().UnixNano())
for {
if numTicket > 0 {
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
numTicket--
fmt.Printf("Saler %d sales one ticket, left %d tickets.\n", i, numTicket)
} else {
fmt.Printf("Saler %d detects no ticket.\n", i)
break
}
}
}

func main() {
numSaler := 4
for i := 0; i < numSaler; i++ {
go saleTicket(i)
}
time.Sleep(5 * time.Second)
}

如这次跑的结果,最后三个售票员检查时还有余票,但卖时已经无票了,导致票成负数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Saler 3 sales one ticket, left 9 tickets.
Saler 0 sales one ticket, left 8 tickets.
Saler 0 sales one ticket, left 7 tickets.
Saler 2 sales one ticket, left 6 tickets.
Saler 1 sales one ticket, left 5 tickets.
Saler 3 sales one ticket, left 4 tickets.
Saler 1 sales one ticket, left 3 tickets.
Saler 0 sales one ticket, left 2 tickets.
Saler 2 sales one ticket, left 1 tickets.
Saler 3 sales one ticket, left 0 tickets.
Saler 3 detects no ticket.
Saler 2 sales one ticket, left -1 tickets.
Saler 2 detects no ticket.
Saler 1 sales one ticket, left -2 tickets.
Saler 1 detects no ticket.
Saler 0 sales one ticket, left -3 tickets.
Saler 0 detects no ticket.

互斥锁

互斥锁 sync.Mutex

多个 goroutine 同时操作同一个资源(临界区)会导致竞态问题,需要通过对资源上锁,确保同一时刻只有一个 goroutine 访问该共享资源。

  • 声明一个互斥锁

    1
    var mutex sync.Mutex
  • 在使用资源前加锁,防止其他 goroutine 同时使用该资源

    1
    mutex.Lock()
  • 在使用完资源后解锁,释放该资源给其他 goroutine 使用

    1
    mutex.Unlock()

上面的用例,在查看票数前加锁,再售票和退出前解锁,如此就不会出现多卖出票的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
"math/rand"
"sync"
"time"
)

var mutex sync.Mutex
var numTicket = 10

func saleTicket(i int) {
rand.Seed(time.Now().UnixNano())
for {
// 加锁
mutex.Lock()
if numTicket > 0 {
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
numTicket--
fmt.Printf("Saler %d sales one ticket, left %d tickets.\n", i, numTicket)
} else {
//解锁
mutex.Unlock()
fmt.Printf("Saler %d detects no ticket.\n", i)
break
}
//解锁
mutex.Unlock()
}
}

func main() {
numSaler := 4
for i := 0; i < numSaler; i++ {
go saleTicket(i)
}
time.Sleep(5 * time.Second)
}

读写互斥锁 sync.RWMutex

互斥锁是完全互斥的,无关是读还是写。但其实并发读并不会出现资源竞争的问题,所以引入读写锁,不限制并发读,但限制并发读写、写写。

Y N
N N
  • 声明一个读写互斥锁

    1
    var mutex sync.RWMutex
  • 在写资源前加写锁,

    1
    mutex.Lock()
  • 在写完资源后解写锁,

    1
    mutex.Unlock()
  • 在读资源前加读锁

    1
    mutex.RLock()
  • 在读完资源后解读锁

    1
    mutex.RUnlock()

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"sync"
"time"
)

var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)

func write() {
rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock() // 解写锁
wg.Done()
}

func read() {
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
wg.Done()
}

func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}

for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}

wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}

原子操作 atomic

原子操作即不能被中断的操作,对资源进行原子操作时,CPU 不会再对该资源进行其他操作。原子操作无锁,通过 CPU 指令直接实现。

通过互斥锁操作会涉及内核态的上下文切换,Go 可以调用 atomic 包在用户态完成原子操作来保证并发安全。

  • 原子操作支持的类型:int32int64uint32uint64uintptrunsafe.Pointer,即整数和指针

int64 为例,取变量地址用于操作

  • 读取

    1
    func atomic.LoadInt64(addr *int64) (val int64)
  • 写入

    1
    func atomic.StoreInt64(addr *int64, val int64)
  • 修改

    1
    func atomic.AddInt64(addr *int64, delta int64) (new int64)
  • 交换

    1
    func atomic.SwapInt64(addr *int64, new int64) (old int64)
  • 比较交换,交换前先检查当前值是否是 old,如是则交换成 new,如不是则不交换

    1
    func atomic.CompareAndSwapInt64(addr *int64, old int64, new int64) (swapped bool)

管道 Channel

Go 的并发模型是 CSP,Communicating Sequential Process,提倡使用通信共享内存,而不是通过共享内存方式进行通信。

Go 可以通过共享内存来实现数据共享,使用锁来防止竞态,但这不可避免的加大了性能问题。由此引入了管道 Channel 的概念,用于 goroutine 之间通信。

创建 Channel

Channel 是引用类型,每个 Channel 都需要定义其允许传输的数据类型。

1
var ch chan 数据类型  //零值为nil

通道的零值为 nil,因此需要使用 make 来定义(缓冲区大小可缺省)

1
ch := make(chan 数据类型 [, 缓冲区大小])

Channel 操作

在定义了 Channel 后,可对其进行下面操作,以 int 类型为例

1
ch := make(chan int)
  • 发送

    1
    ch <- 10
    1. 当存在等待的接收者时,直接将数据发送给阻塞的接收者
    2. 当不存在等待的接收者,但缓冲区存在空余空间时,将数据写入缓冲区
    3. 当不存在等待的接收者,且不存在缓冲区或者缓冲区已满时,阻塞等待其他 goroutine 从 Channel 中接收数据
  • 接收

    1
    2
    3
    <-ch //丢弃通道值
    data := <-ch //接收通道值到data
    data, ok := <-ch //接收通道值到data,ok接收通道是否关闭(false,则通道已关闭)
    1. 当存在等待的发送者时,从阻塞的发送者或者缓冲区中获取数据
    2. 当不存在等待的发送者,但缓冲区存在数据时,从缓冲区接收数据
    3. 当不存在等待的发送者,且不存在缓冲区或者缓冲区不存在数据时,阻塞等待其他 goroutine 向 Channel 中发送数据
  • 关闭,如果不再需要往通道发送值,则可以关闭通道,关闭通道不是必须的,可以自动通过垃圾回收机制回收

    1
    close(ch)

    关闭后的通道:

    1. 对关闭后的通道发送值会导致 panic
    2. 对关闭后的通道接收会一直获取到值,知道通道空
    3. 对关闭后且已经空的通道接收会得到对应数据类型的零值
    4. 关闭已经关闭的通道会导致 panic
Channel缓冲区 nil 非空 非空非满
发送 阻塞 发送值 发送值 阻塞 发送值
接收 阻塞 接收值 阻塞 接收值 接收值
关闭 panic 关闭成功,读取所有数据后,返回零值 关闭成功,返回零值 关闭成功,读取所有数据后,返回零值 关闭成功,读取所有数据后,返回零值

无缓冲的通道

无缓冲通道,又称阻塞通道、同步通道,定义 Channel 时不设置缓冲区大小即为无缓冲通道。

此类 Channel 发送和接收都无缓冲,所以无接收时发送会被阻塞,直到其他 goroutine 从该通道读取数据;同理,无发送时接收会被阻塞,直到其他 goroutine 发送到该通道。

无缓冲Channel

下面错误用例,发送数据到 Channel,无接收,阻塞,无法运行到接收步骤,于是发生死锁报错

1
fatal error: all goroutines are asleep - deadlock!
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
ch := make(chan int)
ch <- 10 //无接收发送,死锁
data := <-ch
fmt.Println(data)
close(ch)
}

需要启动另一个 goroutine 接收,解开死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func recvFromChannel(ch chan int) {
data := <-ch
fmt.Println(data)
wg.Done()
}

func main() {
ch := make(chan int)
wg.Add(1)
go recvFromChannel(ch)
ch <- 10
close(ch)
wg.Wait()
}

有缓冲的通道

定义 Channel 时设置缓冲区大小即可设置为有缓冲通道,在缓冲区有剩余时,即使无接收者,也不阻塞发送。

有缓冲Channel

上述死锁用例,加上缓冲区,即可正常运行。

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
ch := make(chan int, 1) //创建容量为1的缓冲区的通道
ch <- 10
data := <-ch
fmt.Println(data)
close(ch)
}

单向通道

有些 goroutine 函数调用 Channel 仅需发送或者接收,则可以使用单向通道进行限制。

  • 只能发送的通道

    1
    chan<- int
  • 只能接收的通道

    1
    <-chan int

下面例子,两个函数一个仅需要发送,一个仅需要接收,即可传入单向通道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func recvFromChannel(ch <-chan int) { //仅接收通道
data := <-ch
fmt.Println(data)
wg.Done()
}

func sendToChannel(ch chan<- int) { //仅发送通道
ch <- 10
close(ch)
wg.Done()
}

func main() {
ch := make(chan int)
wg.Add(2)
go recvFromChannel(ch)
go sendToChannel(ch)
wg.Wait()
}

通道的遍历

循环从通道取值的方法

方法一:使用 for 持续循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func sendToChannel(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
wg.Done()
}

func main() {
ch := make(chan int)
wg.Add(1)
go sendToChannel(ch)
for {
data, ok := <-ch
if !ok {
break
}
fmt.Println(data)
}
wg.Wait()
}

方法二:for...range,既可以循环取值,并且在通道关闭后退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func sendToChannel(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
wg.Done()
}

func main() {
ch := make(chan int)
wg.Add(1)
go sendToChannel(ch)
for data := range ch {
fmt.Println(data)
}
wg.Wait()
}

通道的调度

通过 select 随机运行一个接收到的 Channel 的 case

  1. 如果有可接收的 Channel,则随机运行其中一个 case
  2. 如果没有可接收的 Channel,则运行 default case
  3. 如果没有可接收的 Channel,且没有 default case,则会阻塞直到有接收到 Channel

使用无限 for 循环包裹 select,则可实现持续监听多个通道,触发相应操作。

1
2
3
4
5
6
7
8
9
10
11
12
for {
select {
case data1, ok := <-ch1:
if !ok {
fmt.Println("ch1 closed.")
break
}
// ch1处理
case <-ch2:
// ch2处理
}
}

sync 包

sync.WaitGroup

在多线程并发过程中,如果主协程结束,其他 goroutine 也会跟着结束,所以经常需要让主协程等待其他 goroutine 结束。使用 time.Sleep 过于生硬,不合适。于是引入了 sync.WaitGroup 处理该问题 。

  • (wg * WaitGroup) Add(delta int):计数器 + delta
  • (wg *WaitGroup) Done():计数器 -1
  • (wg *WaitGroup) Wait():阻塞直到计数器变为 0

每开一个并发 goroutine 就让计数器 +1,并发 goroutine 结束时则让计数器 -1,主协程调用 Wait() 阻塞直到所有 goroutine 结束,计数器清零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup //声明一个WaitGroup

func funcA() {
for i := 0; i < 100; i++ {
fmt.Println(i)
}
wg.Done() //一个goroutine结束,计数器-1
}

func main() {
numGoroutine := 4
wg.Add(numGoroutine) //计数器+并发goroutine数
for i := 0; i < numGoroutine; i++ {
go funcA()
}
wg.Wait() //主协程阻塞直到所有goroutine跑完
}

sync.Once

sync.Once 提供函数只执行一次的方法,如初始化配置、数据库连接此类并发只需要调用一次的函数,可用此方法

1
func (o *Once) Do(f func())

下面示例,funcAfuncB 都需要调用 InitConfig 函数,使用 sync.Once,可使得该函数只被调用一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"sync"
)

var (
wg sync.WaitGroup
once sync.Once //声明once
)

func InitConfig() {
fmt.Println("Init Configuration")
}

func funcA() {
once.Do(InitConfig) //并发只调用一次
fmt.Println("function A")
wg.Done()
}

func funcB() {
once.Do(InitConfig) //并发只调用一次
fmt.Println("function B")
wg.Done()
}

func main() {
wg.Add(2)
go funcA()
go funcB()
wg.Wait()
}

输出

1
2
3
Init Configuration
function B
function A

sync.Map

Go 原生的 map 并不是并发安全的,需要额外加锁。sync 包提供 sync.Map 则是一种并发安全的 map,无需加锁。内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"strconv"
"sync"
)

var (
wg sync.WaitGroup
once sync.Once
)

func main() {
m := sync.Map{} //定义map
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n) //存值
value, _ := m.Load(key) //取值
fmt.Printf("key: %v, value: %v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}

定时器

周期性定时器 ticker

ticker 是周期性定时器,除非主动停止,就会一直循环计时下去。如果希望每隔一段时间执行一次,推荐使用 ticker

用法 作用
func time.NewTicker(d time.Duration) *time.Ticker 定义一个定时器 ticker,每隔一个间隔时间就会向 .C 通道发送当前时间
func (t *time.Ticker) Stop() 回收资源,否则会产生内存泄漏
<-t.C 每隔一个设置的时间就会从通道接收到当前时间

以下示例,每隔 5s 输出当前时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"time"
)

func main() {
ticker := time.NewTicker(5 * time.Second) //定义5s一次的定时器
defer ticker.Stop() //回收资源
go func(t *time.Ticker) {
for {
fmt.Println("Time: ", time.Now().Format("2006-01-02 15:04:05"))
<-t.C //每隔5s触发一次
}
}(ticker)

time.Sleep(15 * time.Second)
}

一次性定时器 timer

timer 是一次性定时器,只计时一次,重新开始计时需要重置。如果希望只执行一次,或者需要重新设置间隔时间的,推荐使用 timer

用法 作用
func time.NewTimer(d time.Duration) *time.Timer 定义一个定时器 timer,过一个间隔时间后会向 .C 通道发送当前时间
<-t.C 过一个设置的时间从通道接收到当前时间
func (t *time.Timer) Stop() bool 停止当前计时,如果当前在计时,则返回 true,并不会再发送到通道;如果不在计时中,则返回 fasle
func (t *time.Timer) Reset(d time.Duration) bool 重置计时器,如果现在正在计时,则停止当前计时,重新计时(返回同 Stop

以下示例,使用一次性计时器 timer 模拟周期性计时器 ticker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"time"
)

func main() {
timer := time.NewTimer(5 * time.Second) //定义一次性计时器timer

fmt.Println("Time: ", time.Now().Format("2006-01-02 15:04:05"))
go func(t *time.Timer) {
times := 0
for {
<-t.C //计时
fmt.Println("Time: ", time.Now().Format("2006-01-02 15:04:05"))

times++
t.Reset(5 * time.Second) //重新开始计时
if times > 3 {
t.Stop() //停止计时
}
}
}(timer)

time.Sleep(30 * time.Second)
}

参考