当前位置: 首页 > article >正文

Golang之Context详解

引言

之前对context的了解比较浅薄,只知道它是用来传递上下文信息的对象;

对于Context本身的存储、类型认识比较少。

最近又正好在业务代码中发现一种用法:在每个协程中都会复制一份新的局部context对象,想探究下这种写法在性能上有没有弊端。

jobList := []func() error{
		s.task1,
		s.task2,
		s.task3,
		s.task4,
	}
if err := gconc.GConcurrency(jobList); err != nil {
    resource.LoggerService.Error(ctx, "exec concurrency job list error", logit.Error("error", err))
}

func (s *Service) task1() (err error) {
	if !s.isLogin() {
		return nil
	}
    // 新局部变量,值来自全局的context对象
	ctx := s.ctx
	return nil
}

基本概念

介绍

golang.org/x/net/context,是golang中的一个标准库,主要作用就是创建一个上下文,实现对程序中创建的协程通过传递上下文信息来实现对协程的管理。

img

创建一个Context对象

在Go语言中,可以通过多种方式创建Context:

空Context对象

Background()和TODO():这两个函数分别用于创建空的Context,通常作为根节点使用

  • Background()通常用于main函数、初始化以及测试;
  • TODO()则用于尚未确定使用哪种Context的情况。
可取消的Contex对象

WithCancel(parent Context):创建一个可取消的Context,并返回一个取消函数

  • 当调用取消函数时,会通知所有的子Context,使它们都取消执行。
带有截止时间的Context对象

WithDeadline(parent Context, deadline time.Time):创建一个带有截止时间的Context,并返回一个取消函数

  • 当超过截止时间时,会自动通知所有的子Context,使它们都取消执行;
  • 当超过截止时间时,会自动通知所有的子Context,使它们都取消执行。
带有超时控制的Contex对象

WithTimeout(parent Context, timeout time.Duration):创建一个带有超时控制的Context,它等同于WithDeadline(parent, time.Now().Add(timeout))。

带键值对的Context对象

WithValue(parent Context, key, val interface{}):创建一个带有键值对的Context,同时保留父级Context的所有数据。

  • 需要注意Context主要用于传递请求范围的数据,而不是用于存储大量数据或传递业务逻辑中的参数。

分析Context对象

上面介绍了几种创建Context对象的方法,包括创建可取消的Context、带有截止时间的Context以及带有键值对的Context

Context接口
type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error
    
	Value(key any) any
}

Context接口定义了四个方法:

  1. Deadline():返回该context被取消的时间,当没有设置截止日期时,返回ok==false。
  2. Done():返回一个只读的channel。当context被取消或超时时,此channel会被关闭,通知goroutine不再继续执行。
  3. Err():如果Done尚未关闭,返回nil;如果context已被取消或超时,返回取消或超时的错误。
  4. Value(key interface{}) interface{}:从context中获取与key相关联的值。通常用于传递一些请求范围内的变量,如用户认证信息、跟踪请求ID等。但需要注意的是,不应滥用此功能传递业务逻辑中的参数。
不同类型的Context

通过分析Context接口,可以知道Context对象都是对Context接口的实现,如空Context对象就是emptyCtx,它不包含任何值

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
}

而带有超时控制的Context其实就是一个带有定时器并且实现了Context接口的对象

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

Context的作用

控制子协程

对于存在若干个协程的程序,协程之前可能会存在如下的关系,这就需要在父协程关闭时,对子协程及时关闭;

否则协程可能会持续存在与内存中,造成内存泄漏。

img

Context对子协程的控制销毁就是基于协程创建的过程中,为每个子协程创建子context,以WithCancel()方法为例进行分析:

WithCancel()会返回一个新的子context和一个上下文取消方法,当执行cancel时,当前协程下的子context都会被销毁。

package main
 
import (
	"context"
	"fmt"
	"time"
)
 
// worker 是一个模拟工作的函数,它接受一个 context 并根据 context 的状态来决定是否继续工作。
func worker(ctx context.Context, id int) {
	for {
		select {
		case <-ctx.Done():
			// 当 context 被取消时,worker 会收到通知并退出循环。
			fmt.Printf("Worker %d: stopping\n", id)
			return
		default:
			// 继续执行模拟的工作。
			fmt.Printf("Worker %d: working\n", id)
			time.Sleep(1 * time.Second)
		}
	}
}
 
func main() {
	// 创建一个带取消功能的 context。
	ctx, cancel := context.WithCancel(context.Background())
 
	// 启动多个 worker 协程。
	for i := 1; i <= 3; i++ {
		go worker(ctx, i)
	}
 
	// 让主协程等待一段时间,然后取消 context。
	time.Sleep(5 * time.Second)
	fmt.Println("Main: canceling context")
	cancel()
 
	// 等待一段时间以确保所有 worker 都有机会响应取消信号。
	// 在实际应用中,你可能需要一个更复杂的机制来等待所有 worker 退出。
	time.Sleep(2 * time.Second)
	fmt.Println("Main: exiting")
}

传递上下文信息

通常使用带键值对的Contex对象,即ValueContext传递信息。

package main

import (
	"context"
	"fmt"
	"time"
)

// requestIDKey 是一个用于在 context 中存储请求 ID 的键。
type requestIDKey struct{}

// worker 是一个模拟工作的函数,它接受一个 context 并从中提取请求 ID。
func worker(ctx context.Context, taskName string) {
	// 从 context 中获取请求 ID。
	requestID := ctx.Value(requestIDKey{}).(string)
	fmt.Printf("%s: started, request ID: %s\n", taskName, requestID)

	// 模拟工作。
	time.Sleep(2 * time.Second)

	// 完成工作。
	fmt.Printf("%s: completed, request ID: %s\n", taskName, requestID)
}

func main() {
	// 创建一个带有请求 ID 的 context。
	requestID := "12345"
	ctx := context.WithValue(context.Background(), requestIDKey{}, requestID)

	// 启动多个 worker 协程。
	tasks := []string{"Task A", "Task B", "Task C"}
	for _, task := range tasks {
		go worker(ctx, task)
	}

	// 等待一段时间以确保所有 worker 都有机会完成工作。
	// 在实际应用中,你可能需要一个更复杂的机制来等待所有 worker 退出。
	time.Sleep(6 * time.Second)
	fmt.Println("Main: all tasks completed or timed out")
}

额外补充:Context变量的复制

写一个示例代码,通过debug来分析

type UserInfo struct {
	UID     int
	Name    string
	Address *Address
}
type Address struct {
	X int
	Y int
}

func TestValueContext(t *testing.T) {
	ctx := context.Background()
	address := &Address{
		X: 101,
		Y: 202,
	}
	withValue := context.WithValue(ctx, UserInfo{}, UserInfo{
		UID:     1,
		Name:    "test",
		Address: address,
	})
    // 新变量 拷贝的context对象
	copyCtx := withValue
	fmt.Println(copyCtx)
}

通过debug可以看到,和普通的变量赋值一样,拷贝出的copyCtx对象就是ctx对象的值;

拷贝的过程是浅拷贝,当ctx中包含指针时,拷贝的是其地址。

img

最开始的问题-拷贝Context的意义?

通过上面的分析我们可以知道以下几点事实

  1. Context的拷贝是针对具体实现了Context接口的对象,因为接口无法拷贝;
  2. Context对象的拷贝是浅拷贝,和普通的变量一样;
  3. 需要基于一个Context实现 添加信息、并发控制、并发安全等功能,需要使用Context库提供的方法,普通的拷贝没有意义。

因此,问题代码中对ctx的拷贝,不考虑代码清晰度的情况下,并没有额外的意义,而且在被拷贝的Context对象很大时,会有额外的内存开销。

func (s *Service) task1() (err error) {
	if !s.isLogin() {
		return nil
	}
    // 新局部变量,值来自全局的context对象
	ctx := s.ctx
	return nil
}

参考

Go语言高并发系列三:context - 掘金


http://www.kler.cn/a/518242.html

相关文章:

  • WPF基础 | WPF 布局系统深度剖析:从 Grid 到 StackPanel
  • 基于springboot+vue的古城景区管理系统的设计与实现
  • 【Rust自学】15.3. Deref trait Pt.2:隐式解引用转化与可变性
  • 【C语言算法刷题】第2题 图论 dijkastra
  • 编译安装PaddleClas@openKylin(失败,安装好后报错缺scikit-learn)
  • 【2024年终总结】深圳工作生活评测
  • 【pytorch 】miniconda python3.11 环境安装pytorch
  • 无公网IP 外网访问媒体服务器 Emby
  • GS论文阅读--GeoTexDensifier
  • 如何实现分页相关功能
  • 比简单工厂更好的 - 工厂方法模式(Factory Method Pattern)
  • Lambda 表达式
  • 笔记《Effective Java》01: 创建和销毁对象
  • 软件测试丨消息管道(Kafka)测试体系
  • 电路研究9.2.1——合宙Air780EP音频的AT控制指令
  • 【工程篇】01:GPU可用测试代码
  • python学opencv|读取图像(四十四)原理探究:bitwise_and()函数实现图像按位与运算
  • UGUI判断点击坐标是否在UI内部,以及子UI内部
  • 运行虚幻引擎UE设置Visual Studio
  • spark sql中对array数组类型操作函数列表
  • android studio搭建NDK环境,使用JNI (Java 调 C) 二 通过CMake
  • Couchbase UI: Server
  • 期权帮|如何利用股指期货进行对冲套利?
  • AI如何改变IT行业
  • 单片机基础模块学习——按键
  • vulnhub靶场【kioptrix-3】靶机