📅 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
 

🌟 无缓冲管道

⭐️ 对于无缓冲管道而言,不会存放任何临时数据,没有缓冲区,当向管道写入和读取数据时必须立刻要又协程来读取和写入数据,否则就会发生阻塞等待,然后死锁,下图为死锁演示图

image-20240513130015774

代码测试

 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) //读取
     }
 }

image-20240513140516736

🌟 有缓冲管道

⭐️ 当管道有了缓冲区,对于有缓冲管道写入数据时,会先将数据放入缓冲区里,只有当缓冲区容量满了才会阻塞的等待协程来读取管道中的数据;读取数据时也会优先从缓冲区来读取数据,直到缓冲区没数据了,才会阻塞的等待协程来向管道中写入数据,这种叫同步写法

 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)
 }
 

image-20240513140651916

⭐️ 通过内置函数 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

下面 lockupunlock就定义了两个单向管道

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

WaitGroupsync包下提供的一个结构体,WaitGroup即等待执行,使用它可以很轻易的实现等待一组协程的效果。该结构体只对外暴露三个方法

  1. Add: 指明要等待的进程数量
  2. Done: 表示当前协程已经执行完毕
  3. 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标准库也提供了几个实现

  1. emptyCtx
  2. cancelCtx
  3. timerCtx
  4. valueCtx

Context接口结构

type Context interface {

   Deadline() (deadline time.Time, ok bool)

   Done() <-chan struct{}

   Err() error

   Value(key any) any
}
  1. Deadline() 获取Context的截止时间(也就是上下文取消时间),如果Context没有设置截止时间,则返回ok的值为false
  2. Done() 返回一个channel,当Context被取消时,该channel会被关闭,通过判断该channel是否关闭来判断Context是否被取消,对于一些不支持取消的上下文,可能会返回nil
  3. Err() 用于表示上下关闭的原因。当Done管道没有关闭时,返回nil,如果关闭过后,会返回一个err来解释为什么会关闭
  4. Value(key any) 该方法返回对应的键值,如果key不存在,或者不支持该方法,就会返回nil

⭐️ emptyCtx

Context所有的实现都是不对外暴露的,但是提供了对应的函数来创建上下文。emptyCtx就可以通过 context.Backgroundcontext.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永远会返回 nilselect会忽略掉 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

timerCtxcancelCtx的基础之上增加了超时机制,context包下提供了两种创建的函数,分别是 WithDeadlineWithTimeout,两者功能类似,前者是指定一个具体的超时时间,比如指定一个具体时间 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))
}

提示

就跟内存分配后不回收会造成内存泄漏一样,上下文也是一种资源,如果创建了但从来不取消,一样会造成上下文泄露,所以最好避免此种情况的发生