📅 2024年5月13日
📦 使用版本为 1.21.5
管道
⭐️ 管道 channel
,通过消息来进行内存共享,它是一个在协程之间通信解决的方案,同时也可以用于并发控制
⭐️ 在 Go
中使用 chan
来代表管道类型,并且还需要一起声明管道内存储数据的类型,它的默认零值为 nil
func main() {
var ch chan int
fmt.Println(ch)
}
⭐️ 所有通道只能传输一种类型的数据,比如 chan int
或者 chan string
,所有的类型都可以用于通道,空接口 interface{}
也可以,甚至可以(有时非常有用)创建通道的通道;
⭐️ 通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO) 的结构所以可以保证发送给他们的元素的顺序
🌟 创建管道
⭐️ 创建管道只有一种方法,就是使用 make
,使用 make
创建管道,需要接收两个参数,一个是管道类型,还有一个是个可选参数管道缓冲的大小
func main() {
var ch chan int
ch = make(chan int, 1) //创建缓冲区大小为1的管道
fmt.Println(ch) //此时输出的就是一个内存地址
}
⭐️ 使用完管道后,和文件一样需要关闭,使用 Close
,配合 defer
更好
func main() {
var ch chan int
ch = make(chan int, 1)
fmt.Println(ch)
defer close(ch) //关闭管道
}
⭐️
🌟 管道的读写
⭐️ 在 Go
中管道提供了两种操作符来表示读写:
ch <-
: 表示对一个管道的写入数据,从外部流入管道<- ch
:表示对一个管道的读取数据,从管道流出
⭐️ 下面的列子体现了两个协程通信,使用了管道
func main() {
var ch chan string
ch = make(chan string)
go setString(ch) //启动一个协程用于设置ch的值
go getString(ch) //启动一个协程用于获取ch的值
time.Sleep(time.Microsecond)
defer close(ch) //关闭ch
}
func getString(ch chan string) (int, error) {
return fmt.Println(<-ch)
}
func setString(ch chan string) chan string {
ch <- "HelloWorld!"
return ch
}
⭐️ 对于读取操作而言,还有第二个返回值,一个布尔类型的值,用于表示数据是否读取成功
ints, ok := <-intCh
🌟 无缓冲管道
⭐️ 对于无缓冲管道而言,不会存放任何临时数据,没有缓冲区,当向管道写入和读取数据时必须立刻要又协程来读取和写入数据,否则就会发生阻塞等待,然后死锁,下图为死锁演示图
代码测试
func main() {
var ch chan string
ch = make(chan string)
ch <- "HelloWorld"
time.Sleep(time.Microsecond)
defer close(ch) //关闭ch
}
//报错
fatal error: all goroutines are asleep - deadlock!
⭐️ 如果是无缓冲的管道,最好使用创建管道内实列的使用方法,使用协程
⭐️ 无缓冲通道(unbuffered channel
)确实要求发送和接收操作必须同时准备好才能进行通信。这是因为无缓冲通道不具备存储元素的能力,它要求发送方和接收方必须同时就绪,否则发送方会被阻塞,直到接收方准备好接收数据。这种设计确保了协程间的同步和数据的一致性,
下面这个代码就很好的体现了,在写入"HelloWorld"之后,它不会立即在写入,因为它没有缓冲区,它会等待其他进程或者协程来读取数据后在写入
func main() {
var ch chan string
ch = make(chan string)
defer close(ch) //关闭ch
go func() {
for i := 0; i < 10; i++ {
fmt.Println("写入成功等待读取中.....")
ch <- "HelloWorld" //写入
}
}()
//主进程等待读写
for i := 0; i < 10; i++ {
//等待10秒读写
time.Sleep(time.Microsecond * 10000000)
fmt.Println("读取成功", <-ch) //读取
}
}
🌟 有缓冲管道
⭐️ 当管道有了缓冲区,对于有缓冲管道写入数据时,会先将数据放入缓冲区里,只有当缓冲区容量满了才会阻塞的等待协程来读取管道中的数据;读取数据时也会优先从缓冲区来读取数据,直到缓冲区没数据了,才会阻塞的等待协程来向管道中写入数据,这种叫同步写法
func main() {
var ch chan string
ch = make(chan string,1) //创建一个缓冲区为1的通道
ch <- "HelloWorld"
time.Sleep(time.Microsecond)
defer close(ch) //关闭ch
}
⭐️ 同步写法时很微信的,一旦缓冲区满了或者空了,没有其他协程来读取和写入数据就会导致永远的阻塞下去
这里两个无缓冲管道用于同步,否则两个协程都不会执行,因为写入无缓冲管道后,如果没有进程去读取它,他就会一直阻塞,然后由于主进程在等待两个无缓冲管道的写入,才执行,所有两个协程都会执行
func main() {
// 创建有缓冲管道
ch := make(chan int, 5)
// 创建两个无缓冲管道
chW := make(chan struct{}) // 两个无缓冲管道用于同步
chR := make(chan struct{}) //
defer func() {
close(ch)
close(chW)
close(chR)
}()
// 负责写
go func() {
for i := 0; i < 10; i++ { //写入数据
ch <- i
fmt.Println("写入", i)
}
chW <- struct{}{} //当上面循写入完成后,由于chW是一个无缓冲管道,写入它之后需要立即读取,如果没有协程来读取,他就会一直阻塞,知道有读取的
}()
// 负责读
go func() {
for i := 0; i < 10; i++ { //读取数据
// 每次读取数据都需要花费1毫秒
time.Sleep(time.Millisecond)
fmt.Println("读取", <-ch)
}
chR <- struct{}{}
}()
fmt.Println("写入完毕", <-chW) //等待两个无缓冲管道执行
fmt.Println("读取完毕", <-chR)
}
⭐️ 通过内置函数 len
可以访问管道缓冲区中数据的个数,通过 cap
可以访问管道缓冲区的大小
func main() {
ch := make(chan int, 5)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(len(ch), cap(ch))
}
⭐️ 互斥锁
这里具体内容后面会说
⭕️ 通过有缓存管道还可以实现一个互斥锁
var count = 0
// 缓冲区大小为1的管道
var lock = make(chan struct{}, 1)
func Add() {
// 加锁
lock <- struct{}{}
fmt.Println("当前计数为", count, "执行加法")
count += 1
time.Sleep(3 * time.Second) //等待三秒后才解锁
// 解锁
<-lock
}
func Sub() {
//Add解锁后,立马拿到锁执行
lock <- struct{}{}
fmt.Println("当前计数为", count, "执行减法")
count -= 1
// 解锁
<-lock
}
func main() {
Add()
Sub()
}
⭐️ 无缓冲和有缓冲管道注意点总结
1️⃣****读写无缓冲管道
当对一个无缓冲管道直接进行同步读写操作都会导致该协程阻塞
func main() {
// 创建了一个无缓冲管道
intCh := make(chan int)
defer close(intCh)
// 发送数据
intCh <- 1
// 读取数据
ints, ok := <-intCh
fmt.Println(ints, ok)
}
2️⃣读取空缓冲区的管道
当读取一个缓冲区为空的管道时,会导致该协程阻塞
func main() {
// 创建的有缓冲管道
intCh := make(chan int, 1)
defer close(intCh)
// 缓冲区为空,阻塞等待其他协程写入数据
ints, ok := <-intCh
fmt.Println(ints, ok)
}
3️⃣****写入满缓冲区的管道
当管道的缓冲区已满,对其写入数据会导致该协程阻塞
func main() {
// 创建的有缓冲管道
intCh := make(chan int, 1)
defer close(intCh)
intCh <- 1
// 满了,阻塞等待其他协程来读取数据
intCh <- 1
}
4️⃣****管道为 nil
当管道为 nil
时,无论怎样读写都会导致当前协程阻塞
func main() {
var intCh chan int
// 写
intCh <- 1
}
func main() {
var intCh chan int
// 读
fmt.Println(<-intCh)
}
⭐️导致 panic
1️⃣关闭一个 nil
管道
**当管道为 **nil
时,使用 close
函数对其进行关闭操作会导致 panic
func main() {
var intCh chan int
close(intCh)
}
2️⃣写入已关闭的管道
**对一个已关闭的管道写入数据会导致 **panic
func main() {
intCh := make(chan int, 1)
close(intCh)
intCh <- 1
}
3️⃣关闭已关闭的管道
在一些情况中,管道可能经过层层传递,调用者或许也不知道到底该由谁来关闭管道,如此一来,可能会发生关闭一个已经关闭了的管道,就会发生 panic
。
func main() {
ch := make(chan int, 1)
defer close(ch)
go write(ch)
fmt.Println(<-ch)
}
func write(ch chan<- int) {
// 只能对管道发送数据
ch <- 1
close(ch)
}
4️⃣ 可以向关闭的有缓存区管道读取数据
package main
import (
"fmt"
)
func main() {
m := make(chan int,10)
m <- 1
close(m)
select {
case v := <- m:
fmt.Println(v)
default:
fmt.Println("default")
}
}
🌟单向管道
⭐️ 单向管道是一个只读或者只写的管道,即只能在管道的一边进行操作,它通常只用于一种约束手段
⭐️ 动创建的一个只读或只写的管道没有什么太大的意义,因为不能对管道读写就失去了其存在的作用。单向管道通常是用来限制通道的行为,一般会在函数的形参和返回值中出现,在 Close
函数的函数签名中就体现了,它的形参就是一个单向管道
func close(c chan<- Type)
⭐️ 单向管道定义方法:
- 箭头符号
<-
在前,就是只读通道,如<-chan int
- 箭头符号
<-
在后,就是只写通道,如chan<- string
下面 lockup
和 unlock
就定义了两个单向管道
var count = 0
// 缓冲区大小为1的管道
var lock = make(chan struct{}, 1)
func lockup(lock chan<- struct{}) { //只写管道
lock <- struct{}{}
}
func unlock(lock <-chan struct{}) { //只读管道
<-lock
}
func Add() {
// 加锁
lockup(lock)
fmt.Println("当前计数为", count, "执行加法")
count += 1
time.Sleep(3 * time.Second) //等待三秒后才解锁
// 解锁
unlock(lock)
}
func Sub() {
//Add解锁后,立马拿到锁执行
lockup(lock)
fmt.Println("当前计数为", count, "执行减法")
count -= 1
// 解锁
unlock(lock)
}
func main() {
Add()
Sub()
defer close(lock)
}
⭐️ 当尝试对只读的管道写入数据时,将会无法通过编译,读只写的读也是一样
⭐️ 双向管道可以转换为单向管道,反过来则不可以,通常情况下,将双向管道传给某个协程或函数并且不希望它读取/发送数据,就可以用到单向管道来限制另一方的行为
🌟 遍历管道数据
⭐️ 使用 for range
可以遍历读取缓冲管道中的数据
func main() {
ch := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
ch <- i //写入
}
// 关闭管道
close(ch)
}()
for n := range ch {//等待接收读取ch管道内的值
fmt.Println(n)
}
}
⭐️ 前面提到过读取管道是有两个返回值的,for range
遍历管道时,当无法成功读取数据时,便会退出循环。第二个返回值指的是能否成功读取数据,而不是管道是否已经关闭,即便管道已经关闭,对于有缓冲管道而言,依旧可以读取数据,并且第二个返回值仍然为 true
func main() {
ch := make(chan int, 10)
for i := 0; i < 5; i++ {
ch <- i
}
// 关闭管道
close(ch)
// 再读取数据
for i := 0; i < 6; i++ {
n, ok := <-ch
fmt.Println(n, ok)
}
}
//输出:
0 true
1 true
2 true
3 true
4 true
0 false
🌟 监测管道
⭐️ 在 go
中可以使用 select
来监测管道且只能是管道,它可以很好的监测到每个管道的状态
⭐️ 它在 Go
的语言层面上提供了 I/O多路复用技术
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chC)
close(chB)
close(chA)
}()
//检测管道是否可用,每一个case只能操作一个管道,且只能进行一种操作,要么读要么写入
select {
case ele, ok := <-chA:
fmt.Println(ele, ok)
case ele, ok := <-chB:
fmt.Println(ele, ok)
case ele, ok := <-chC:
fmt.Println(ele, ok)
default:
fmt.Println("所有管道都不可用")
}
}
⭐️ 当有多个 case
可用时,select
会伪随机的选择一个 case
来执行。如果所有 case
都不可用,就会执行 default
分支,倘若没有 default
分支,将会阻塞等待,直到至少有一个 case
可用
⚠️
select
中的case
不会对值为nil
的管道进行阻塞操作,而是会直接忽略
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chC)
close(chB)
close(chA)
}()
go func() {
//三秒后写入
time.Sleep(3 * time.Second)
chA <- 1
}()
//检测管道是否可用,每一个case只能操作一个管道,且只能进行一种操作,要么读要么写入
select {
case ele, ok := <-chA: //此时select会一直阻塞,等待chA写入数据,一旦写入就执行
fmt.Println(ele, ok)
case ele, ok := <-chB:
fmt.Println(ele, ok)
case ele, ok := <-chC:
fmt.Println(ele, ok)
}
}
1️⃣ 什么那种一旦 A
执行了,就会立即退出 select
,只能监测一个,如果想监测多个,可用使用 for
循环,这里需要使用到进程锁
**不能直接使用 **
break
语句来退出select
语句的循环。这是因为select
语句本身是一个阻塞操作,它会一直等待某个通道完成操作,直到其中一个case
分支被执行或者超时(通过time.After
实现)。如果你直接使用break
语句,它会尝试跳出当前的for
循环,而不是select
语句本身。**为了跳出 **
select
语句的循环,你需要使用标签(label)来标记for
循环的开始,然后在需要跳出循环的地方使用break
语句加上该标签。这样,当满足某个条件时,break
语句会跳出整个for
循环,从而结束select
语句的执行
⭐️ 使用 time.Afer
来设置超时时间
// 缓冲区大小为1的管道,也可以使用无缓冲管道
var lock = make(chan struct{},1)
func lockup(lock chan<- struct{}) { //只写管道
lock <- struct{}{}
}
func unlock(lock <-chan struct{}) { //只读管道
<-lock
}
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chC)
close(chB)
close(chA)
}()
go func() {
//三秒后写入
time.Sleep(3 * time.Second)
chA <- 1
chB <- 1
chC <- 1
}()
go func() {
//检测管道是否可用,每一个case只能操作一个管道,且只能进行一种操作,要么读要么写入
Loop:
for {
select {
case ele, ok := <-chA: //此时select会一直阻塞,等待chA写入数据,一旦写入就执行
fmt.Println(ele, ok)
case ele, ok := <-chB:
fmt.Println(ele, ok)
case ele, ok := <-chC:
fmt.Println(ele, ok)
case <-time.After(4 * time.Second): //设置4秒超时时间
break Loop
}
}
lockup(lock) //拿到锁,通知主程序解锁
}()
unlock(lock) //主程序解锁,然后结束程序
}
⭐️ 永久阻塞
⭐️ 当select语句中什么都没有时,就会阻塞
select{}
🌟 WaitGroup
WaitGroup
是 sync
包下提供的一个结构体,WaitGroup
即等待执行,使用它可以很轻易的实现等待一组协程的效果。该结构体只对外暴露三个方法
- Add: 指明要等待的进程数量
- Done: 表示当前协程已经执行完毕
- Wait方法:等待子协程结束,否则就阻塞
WaitGroup
使用起来十分简单,属于开箱即用。其内部的实现是计数器+信号量,程序开始时调用 Add
初始化计数,每当一个协程执行完毕时调用 Done
,计数就-1,直到减为0,而在此期间,主协程调用 Wait
会一直阻塞直到全部计数减为0,然后才会被唤醒。看一个简单的使用例子
**这下就不用使用 **sleep
来等待协程完毕操作了,这里在协程中 Sleep
可以更加清楚的看到在等待协程5秒主进程才执行完毕,输出结果也永远是先输出协程输出的内容在输出主进程内的
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
fmt.Println("协程执行ing....")
time.Sleep(time.Second * 5)
wg.Done()
}()
//协程未完成则一直柱塞
wg.Wait()
fmt.Println("协程执行完毕,主进程执行完毕")
}
WaitGroup
通常适用于可动态调整协程数量的时候,例如事先知晓协程的数量,又或者在运行过程中需要动态调整。WaitGroup
的值不应该被复制,复制后的值也不应该继续使用,尤其是将其作为函数参数传递时,因该传递指针而不是值。倘若使用复制的值,计数完全无法作用到真正的 WaitGroup
上,这可能会导致主协程一直阻塞等待,程序将无法正常运行
func main() {
var mainWait sync.WaitGroup
mainWait.Add(1)
hello(mainWait)
mainWait.Wait()
fmt.Println("end")
}
// 应该是需要接收指针类型
func hello(wait sync.WaitGroup) {
fmt.Println("hello")
wait.Done()
}
🌟 Context
Context是Go提供的一种并发控制的解决方案,相比于管道和WaitGroup,它可以更好的控制子孙协程以及层级更深的协程。Context本身是一个接口,只要实现了该接口都可以称之为上下文例如著名Web框架Gin中的gin.Context。context标准库也提供了几个实现
- emptyCtx
- cancelCtx
- timerCtx
- valueCtx
Context接口结构
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
- Deadline() 获取Context的截止时间(也就是上下文取消时间),如果Context没有设置截止时间,则返回ok的值为false
- Done() 返回一个channel,当Context被取消时,该channel会被关闭,通过判断该channel是否关闭来判断Context是否被取消,对于一些不支持取消的上下文,可能会返回nil
- Err() 用于表示上下关闭的原因。当Done管道没有关闭时,返回nil,如果关闭过后,会返回一个err来解释为什么会关闭
- Value(key any) 该方法返回对应的键值,如果key不存在,或者不支持该方法,就会返回nil
⭐️ emptyCtx
Context
所有的实现都是不对外暴露的,但是提供了对应的函数来创建上下文。emptyCtx
就可以通过 context.Background
和 context.TODO
来进行创建,下面是它们两个的源代码
//可以看到它们两个的实现都是空结构体,放回的也都是自身空结构
type backgroundCtx struct{ emptyCtx }
func (backgroundCtx) String() string {
return "context.Background"
}
type todoCtx struct{ emptyCtx }
func (todoCtx) String() string {
return "context.TODO"
}
func Background() Context {
return backgroundCtx{}
}
func TODO() Context {
return todoCtx{}
}
emptyCtx结构体底层实际上是一个空结构体,它没法被取消,没有deadline,也不能取值,实现的方法都是返回零值
// An emptyCtx is never canceled, has no values, and has no deadline.
// It is the common base of backgroundCtx and todoCtx.
type emptyCtx struct{}
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (emptyCtx) Done() <-chan struct{} {
return nil
}
func (emptyCtx) Err() error {
return nil
}
func (emptyCtx) Value(key any) any {
return nil
}
emptyCtx通常是用来当作最顶层的上下文,在创建其他三种上下文时作为父上下文传入。context包中的各个实现关系如下图所示
⭐️ valueCtx
valueCtx
只包含了一堆键值对和内嵌的一个 Context
类型的字段
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val any
}
它实现了两个方法
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
它可以使用 WithValue
创建,前面也介绍了 emptyCtx
是在创建其他上下文时作为父上下文传入的
func main() {
// 需要传入一个key value值
c := context.WithValue(context.Background(), "num", 1)
fmt.Println(c)
}
valueCtx
的简单使用案列
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go DoCtx(&wg, context.WithValue(context.Background(), "num", 2))
wg.Wait()
}
func DoCtx(wg *sync.WaitGroup, ctx context.Context) {
defer wg.Done()
// 创建定时器
ticker := time.NewTicker(time.Second)
for {
select {
case <-ctx.Done():
case <-ticker.C:
fmt.Println("timeout")
return
default:
fmt.Println(ctx.Value("num"))
}
time.Sleep(time.Millisecond * 100)
}
}
valueCtx
多用于在多级协程中传递一些数据,无法被取消,因此 ctx.Done
永远会返回 nil
,select
会忽略掉 nil
管道
2
2
2
2
2
2
2
2
2
2
timeout
⭐️ cancelCtx
cancelCtx
以及 timerCtx
都实现了 canceler
接口,接口类型如下
type canceler interface {
// removeFromParent 表示是否从父上下文中删除自身
// err 表示取消的原因
cancel(removeFromParent bool, err error)
// Done 返回一个管道,用于通知取消的原因
Done() <-chan struct{}
}
下面是 cancelCtx
结构
type cancelCtx struct {
Context
mu sync.Mutex //保护接下来的字段
done atomic.Value //惰性创建的chan struct{},在第一次cancel调用时关闭
children map[canceler]struct{} //第一次调用cancel时将其设置为nil
err error //第一次调用cancel时将其设置为non-nil
cause error //第一次调用cancel时将其设置为non-nil
}
cancel
方法不对外暴露,在创建上下文时通过闭包将其包装为返回值以供外界调用,就如 context.WithCancel
源代码中所示
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
// 尝试将自身添加进父级的children中
propagateCancel(parent, &c)
// 返回context和一个函数
return &c, func() { c.cancel(true, Canceled) }
}
放回了一个可以关闭Done的的函数
// cancel关闭c.done,取消c的每个子元素
// removeFromParent为true时,将c从父元素的子元素中移除
//如果c是第一次被取消,cancel会将c设置为cause。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
.....
}
cancelCtx
译为可取消的上下文,创建时,如果父级实现了 canceler
,就会将自身添加进父级的 children
中,否则就一直向上查找。如果所有的父级都没有实现 canceler
,就会启动一个协程等待父级取消,然后当父级结束时取消当前上下文。当调用 cancelFunc
时,Done
通道将会关闭,该上下文的任何子级也会随之取消,最后会将自身从父级中删除
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
//创建cancelCtx,放回ctx和一个关闭方法
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
defer wg.Done()
// 创建定时器
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("等待取消中....")
}
time.Sleep(time.Millisecond * 200)
}
}()
time.Sleep(time.Second)
cancelFunc()
wg.Wait()
}
结果
等待取消中....
等待取消中....
等待取消中....
等待取消中....
等待取消中....
context canceled
嵌套更深一点的示例
var waitGroup sync.WaitGroup
func main() {
//表示有3个协程在执行
waitGroup.Add(3)
// 这个时父上下文的进程
ctx, cancelFunc := context.WithCancel(context.Background())
go HttpHandler(ctx)
time.Sleep(time.Second)
cancelFunc()
waitGroup.Wait()
}
func HttpHandler(ctx context.Context) {
// 创建认证和邮箱上下文
cancelCtxAuth, cancelAuth := context.WithCancel(ctx)
cancelCtxMail, cancelMail := context.WithCancel(ctx)
// 在父上下文关闭后关闭子上下文
defer cancelAuth()
defer cancelMail()
defer waitGroup.Done()
// 启动两个使用认证和邮箱上下文的协程
go AuthService(cancelCtxAuth)
go MailService(cancelCtxMail)
for {
select {
//父上下文关闭
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("正在处理http请求...")
}
time.Sleep(time.Millisecond * 200)
}
}
func AuthService(ctx context.Context) {
defer waitGroup.Done()
for {
select {
//如果父上下文关闭子上下文也关闭
case <-ctx.Done():
fmt.Println("auth 父级取消", ctx.Err())
return
default:
fmt.Println("auth...")
}
time.Sleep(time.Millisecond * 200)
}
}
func MailService(ctx context.Context) {
defer waitGroup.Done()
for {
select {
case <-ctx.Done():
fmt.Println("mail 父级取消", ctx.Err())
return
default:
fmt.Println("mail...")
}
time.Sleep(time.Millisecond * 200)
}
}
⭐️ timerCtx
timerCtx
在 cancelCtx
的基础之上增加了超时机制,context
包下提供了两种创建的函数,分别是 WithDeadline
和 WithTimeout
,两者功能类似,前者是指定一个具体的超时时间,比如指定一个具体时间 2023/3/20 16:32:00
,后者是指定一个超时的时间间隔,比如5分钟后。两个函数的签名如下
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
timerCtx
会在时间到期后自动取消当前上下文,取消的流程除了要额外的关闭 timer
之外,基本与 cancelCtx
一致。下面是一个简单的 timerCtx
的使用示例
var wait sync.WaitGroup
func main() {
// 设置1秒后上下文过期
deadline, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
// 为了保险起见手动在关闭远程
defer cancel()
wait.Add(1)
go func(ctx context.Context) {
defer wait.Done()
for {
select {
case <-ctx.Done():
fmt.Println("上下文取消", ctx.Err())
return
default:
fmt.Println("等待取消中...")
}
time.Sleep(time.Millisecond * 200)
}
}(deadline)
wait.Wait()
}
WithTimeout
其实与 WithDealine
非常相似,它的实现也只是稍微封装了一下并调用 WithDeadline
,和上面例子中的 WithDeadline
用法一样,如下
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
提示
就跟内存分配后不回收会造成内存泄漏一样,上下文也是一种资源,如果创建了但从来不取消,一样会造成上下文泄露,所以最好避免此种情况的发生