• Go 学习之 Context 篇


    参考

    context 接口介绍

    Context被称之为上下文,用它我们可以,它可以实时跟踪我们的每一个Go协程,有了这个特性,我们只需要了解怎么使用就可以了。

    context接口如下:

    type Context interface {
        Deadline() (deadline time.Time, ok bool)
        Done() <-chan struct{}
        Err() error
        Value(key interface{}) interface{}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • Deadline方法会返回设置的截止时间,如果协程运行到了这个时间点,Context则会自动触发取消,实现协程的关闭。
    • Done方法是一个只读的通道,它意味着如果可以读,则说明我们的context发起的取消。
    • Err()方法是在运营中出现错误会及时的返回
    • Value()方法则可以在跟踪协程是带上key-value的值。

    基础测试

    验证 ctx.Done() 可收到结束信号,从而实现程序的退出控制

    1. 首先我们使用了context.Backgroud()创建了一个上下文根节点,这个根节点的值时空的
    2. 利用WithTimeout()基于根节点创建了一个【2s后自动取消的】上下文
    3. 创建一个 ticker 500 ms 定时器,可以自行添加逻辑探测程序是否存活
    4. 最后 2s 时间到了,<-ctx.Done() 收到信号,程序结束
    func main() {
    	ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
    	startTime := time.Now()
    	// 并没有使用,现实中,可以用来接收 如查询数据库传来的数据
    	query := make(chan int)
    	// 定时器  每 500 ms 触发一次
    	ticker := time.NewTicker(500 * time.Millisecond)
    	for {
    		select {
    		case <-ctx.Done():
    			fmt.Println("收到了停止信号,超时结束")
    			cost := int(time.Since(startTime) / time.Second)
    			fmt.Println("用时:", cost, "s")
    			return
    		case <-query:
    			fmt.Println("读取数据")
    		case <-ticker.C:
    			fmt.Println("探活,每 500 ms 检查一次")
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    控制协程

    控制单个协程

    使用 ctx.Done() 可控制协程的结束

    1. 首先我们使用了context.Backgroud()创建了一个上下文根节点,这个根节点的值时空的
    2. 利用WithCancel()基于根节点创建了一个可以取消的上下文,在go协程中我们使用了这个上下文来进行跟踪
    3. 利用select判断是否<-ctx.Done收到了取消的信号
    4. 使用 time.Sleep(5 * time.Second) 保证 main 进程一直在运行中,使 goroutine 一直运行
    func main() {
      ctx, cancel := context.WithCancel(context.Background())
      go func(ctx context.Context) {
        for {
          select {
          case <-ctx.Done():
            fmt.Println("查询到有协程停止")
            return
          default:
            fmt.Println("gorouting正在有效时间内运行")
            time.Sleep(2 * time.Second)
          }
        }
      }(ctx)
    
      time.Sleep(20 * time.Second)
      // 关闭 ctx,此时ctx.Done()会收到停止信号
      cancel()
      // 为了保证 main 主进程在运行中,若 main 函数结束, goroutine 会自动退出的
      time.Sleep(5 * time.Second)
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    控制多个协程

    只需要发一次信号,即可控制多个协程的结束

    在这个事例中,共启动了3个协程,这3个协程,分别都用了一个Context上下文进行跟踪,此时如果cancel函数被使用,3个协程都均会关闭,所以,上下文不仅能关联一个协程,同时也能控制多个协程的关闭。

    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    	go Run(ctx, "协程1")
    	go Run(ctx, "协程2")
    	go Run(ctx, "协程3")
    
    	time.Sleep(10 * time.Second)
    	fmt.Println("开始取消协程的运行")
    	cancel()
    	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
    	time.Sleep(5 * time.Second)
    }
    
    func Run(ctx context.Context, name string) {
    	for {
    		select {
    		case <-ctx.Done():
    			fmt.Println(name, "收到结束信号,停止")
    			return
    		default:
    			fmt.Println(name, "goroutine 正在有效时间内运行")
    			time.Sleep(2 * time.Second)
    		}
    	}
    }
    
    // Output:
    // 协程2 goroutine 正在有效时间内运行
    // 协程1 goroutine 正在有效时间内运行
    // 协程3 goroutine 正在有效时间内运行
    // 开始取消协程的运行
    // 协程3 收到结束信号,停止
    // 协程2 收到结束信号,停止
    // 协程1 收到结束信号,停止
    
    
    • 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

    Context的继承衍生

    在上面的事例中我们均用到了一个函数那就是context.Background(),其实在context包中,不仅仅只提供了这一个函数来提供创建空根节点,还有一个叫做TODO的函数,这个函数和Background一样 都是一个结构体,且他们的特点都是不可取消,没有截止时间,也没有携带任何的Contxet,所以也可以被用作根节点

    有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With系列的函数了。

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    func WithValue(parent Context, key, val interface{}) Context
    
    • 1
    • 2
    • 3
    • 4

    这四个With函数,接收的【都有一个partent参数,就是父Context,我们要基于这个父Context创建出子Context的意思】,这种方式可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。

    通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

    WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。

    WithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。

    WithTimeoutWithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。

    WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到,后面我们会专门讲。

    大家可能留意到,前三个函数都返回一个取消函数CancelFunc,这是一个函数类型,它的定义非常简单。

    type CancelFunc func()
    
    • 1

    这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的所有的Context,不管有多少层级。

    WithValue传递元数据

    通过Context我们也可以传递一些必须的元数据,这些数据会附加在Context上以供使用。

    var key string="name"
    
    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    	//附加值
    	valueCtx:=context.WithValue(ctx,key,"【监控1】")
    	go watch(valueCtx)
    	time.Sleep(10 * time.Second)
    	fmt.Println("可以了,通知监控停止")
    	cancel()
    	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
    	time.Sleep(5 * time.Second)
    }
    
    func watch(ctx context.Context) {
    	for {
    		select {
    		case <-ctx.Done():
    			//取出值
    			fmt.Println(ctx.Value(key),"监控退出,停止了...")
    			return
    		default:
    			//取出值
    			fmt.Println(ctx.Value(key),"goroutine监控中...")
    			time.Sleep(2 * time.Second)
    		}
    	}
    }
    
    • 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

    在前面的例子,我们通过传递参数的方式,把name值传递给监控函数。在这个例子里,我们实现一样的效果,但是通过的是Context的Value的方式。

    我们可以使用context.WithValue方法附加一对K-V的键值对,这里Key必须是等价性的,也就是具有可比性;Value值要是线程安全的。

    这样我们就生成了一个新的Context,这个新的Context带有这个键值对,在使用的时候,可以通过Value方法读取ctx.Value(key)

    记住,使用WithValue传值,一般是必须的值,不要什么值都传递。

    Context 使用原则

    1. 不要把Context放在结构体中,要以参数的方式传递
    2. 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
    3. 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
    4. Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
    5. Context是线程安全的,可以放心的在多个goroutine中传递
  • 相关阅读:
    A O P
    linux在anaconda环境中配置GPU版本的cuda+cudnn+pytorch深度学习环境(简单可行!一次完成!)
    密码学 | 期末考前小记
    MySQL中JOIN的用法
    关于比较两个对象属性
    flink中的Time和watermark
    【IEEE独立出版、有确定的ISBN号】第三届能源与电力系统国际学术会议 (ICEEPS 2024)
    11、IOC 之使用 JSR 330 标准注释
    vue3项目到React 的nextjs项目的改版升级后,网站不更新,如何清理缓存,让改版后的网站生效?
    IOC容器(详细讲解)
  • 原文地址:https://blog.csdn.net/qq_24433609/article/details/126764187