CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
GitHub (opens new window)
  • Go开发实用指南 说明
  • 第1章 Go项目如何组织
  • 第2章 字符串处理
  • 第3章 处理日期和时间
  • 第4章 使用数组、切片和映射
  • 第5章 使用类型、结构体和接口
  • 第6章 使用泛型
  • 第7章 并发
  • 第8章 错误与恐慌(panic)
  • 第9章 context包
    • 使用上下文传递请求作用域的数据
      • 如何操作...
      • 它的工作原理...
      • 更多内容...
    • 使用上下文进行取消操作
      • 操作方法……
      • 工作原理……
    • 使用上下文设置超时
      • 操作方法……
      • 工作原理……
      • 更多内容……
    • 在服务器中使用取消和超时机制
      • 操作方法……
    • 推荐阅读
  • 第10章 处理大量数据
  • 第11章 处理JSON数据
  • 第12章 进程
  • 第13章 网络编程
  • 第14章 流式输入/输出
  • 第15章 数据库
  • 第16章 日志记录
  • 第17章 测试、基准测试和性能分析
目录

第9章 context包

# 第9章 context包

上下文(Context)指的是某件事情发生的环境和背景。当我们讨论程序时,上下文指的就是程序的运行环境、配置等。对于服务器程序(比如响应客户端请求的HTTP服务器、响应函数调用的RPC服务器等),或者响应用户请求的程序(交互式程序、命令行应用程序等)而言,我们可以提及特定请求的上下文。特定请求的上下文在服务器或程序开始处理某个特定请求时创建,并在处理结束时终止。请求上下文包含诸如请求标识符等信息,这些信息有助于识别在处理请求过程中生成的日志消息,或者包含调用者的身份信息,以便确定调用者的访问权限。context包的用途之一,就是对这样的请求上下文进行抽象,即创建一个用于保存特定请求数据的对象。

你可能还会关注请求的运行时间。通常,你会希望限制请求的处理时长,或者检测客户端是否不再关注请求的结果(例如WebSocket连接的对等方断开连接)。context包也旨在处理这些情况。

context包定义了context.Context接口。它有两个主要用途:

  • 为请求处理添加超时和 / 或取消功能
  • 向下传递特定请求的元数据

context.Context的使用并不局限于服务器程序。“请求处理” 这一术语应从广义上去理解:请求可以是通过TCP连接的网络请求、HTTP请求、从命令行读取的命令、使用特定标志运行的程序,等等。因此,context.Context的用途更为广泛。

本章将介绍context包的常见用法。在本章中,你将学习以下内容:

  • 使用上下文传递请求作用域的数据
  • 使用上下文进行取消操作和设置超时

# 使用上下文传递请求作用域的数据

请求作用域的对象是指在请求处理开始时创建,在请求处理结束时丢弃的对象。这些通常是轻量级的对象,例如请求标识符、用于识别调用者的身份验证信息或日志记录器。在本节中,你将了解如何使用上下文来传递这些对象。

# 如何操作...

向上下文添加数据值的惯用方法如下:

  1. 定义上下文键类型。这可以避免意外的名称冲突。通常会使用类似下面这样未导出的类型名称。这种模式将设置或获取特定类型上下文值的能力限制在当前包内:
type requestIDKeyType int
1
警告
你可能会想用struct{}来代替int。毕竟,struct{}不会占用任何额外内存。但在使用零大小的结构体时必须格外小心,因为Go语言规范并未对两个零大小结构体的等价性提供任何保证。也就是说,如果你创建多个零大小类型的变量,它们有时可能相等,有时可能不相等。简而言之,不要在这种情况下使用struct{}。
  1. 使用键类型定义键值。在下面的代码行中,requestIDKey被定义为requestIDKeyType类型,其值为0(requestIDKey在声明时被初始化为其零值):
var requestIDKey requestIDKeyType
1
  1. 使用context.WithValue将新值添加到上下文中。你可以定义几个辅助函数来设置和获取上下文中的值:
func WithRequestID(ctx context.Context, requestID string) context.Context {
	return context.WithValue(ctx,requestIDKey, requestID)
}

func GetRequestID(ctx context.Context) string {
    id,_: = ctx.Value(requestIDKey).(string)
    return id
}
1
2
3
4
5
6
7
8
  1. 将新的上下文传递给从当前函数调用的函数:
newCtx: = WithRequestID(ctx, requestID)
handleRequest(newCtx)
1
2

# 它的工作原理...

你可能已经注意到,context.Context并不完全像一个键值映射(它没有SetValue方法;实际上,context.Context是不可变的),尽管你可以用它来存储键值对。事实上,你不能向上下文添加键值,但你可以获取一个包含该键值的新上下文,同时保留旧上下文。上下文就像洋葱一样具有层次结构;每次向上下文添加内容时,都会创建一个新的上下文,该上下文与旧上下文相关联,但具有更多的特性:

// ctx: 一个空上下文
ctx := context.Background()
// ctx1: ctx + {key1:value1}
ctx1 := context.WithValue(ctx, "key1", "value1")
// ctx2: ctx1 + {key2:value2}
ctx2 := context.WithValue(ctx, "key2", "value2")
1
2
3
4
5
6

在前面的代码中,ctx、ctx1和ctx2是三个不同的上下文。ctx上下文为空。ctx1包含ctx以及键值对key1: value1。ctx2包含ctx1以及键值对key2: value2。假设你执行以下操作:

val1,_ := ctx2.Value("key1")
val2,_ := ctx2.Value("key2")
fmt.Println(val1, val2)
1
2
3

这将输出:

value1 value2
1

假设你对ctx1执行相同操作:

val1,_ = ctx1.Value("key1")
val2,_ = ctx1.Value("key2")
fmt.Println(val1, val2)
1
2
3

这将输出:

value1 <nil>
1

对ctx执行以下操作:

val1,_ = ctx.Value("key1")
val2,_ = ctx.Value("key2")
fmt.Println(val1, val2)
1
2
3

这将输出:

<nil> <nil>
1

提示

尽管你不能在上下文中设置值(即上下文是不可变的),但你可以设置一个指向结构体的指针,并在该结构体中设置值。

即:

type ctxData struct {
	value int
}

...
ctx: = context.WithValue(context.Background(),dataKey, &ctxData{})
...

if data, exists: = ctx.Value(dataKey); exists {
	data.(*ctxData).value = 1
}
1
2
3
4
5
6
7
8
9
10
11

标准库提供了几个预定义的上下文值:

  • context.Background()返回一个没有值且无法取消的上下文。这通常是大多数操作的基础上下文。
  • context.TODO()与context.Background()类似,其名称表明,无论在何处使用它,最终都应重构为接受真实的上下文。

# 更多内容...

上下文通常在多个goroutine之间共享。如果你在上下文中放入对象指针,就必须注意并发问题。看下面这个示例,它展示了一个HTTP服务的身份验证中间件:

type AuthInfo struct {
    // 创建AuthInfo时设置
    UserID string
    // 延迟初始化
    privileges map[string]Privilege
}

type authInfoKeyType int
var authInfoKey authInfoKeyType

// 设置权限(如果尚未初始化)。
// 不要这样做!!
func (auth *AuthInfo) GetPrivileges() map[string]Privilege {
    if auth.privileges == nil {
        // 可能有问题:如果多个调用AuthInfo.GetPrivileges的goroutine可能会尝试多次初始化映射,
        // 导致相互覆盖。
    	auth.privileges = GetPrivileges(auth.UserID)
    }
    
    return auth.privileges
}

// 身份验证中间件
func AuthMiddleware(next http.Handler) func(http.Handler) http.Handler
{
    return http.HandleFunc(func(w http.ResponseWriter, r *http.Request) {
        // 对调用者进行身份验证
        var authInfo *AuthInfo
        var err error
        authInfo, err = authenticate(r)
        if err != nil {
            http.Error(w,err.Error(),http.StatusUnauthorized)
            return
        }
        
        // 使用身份验证信息创建一个新的上下文
        newCtx: = context.WithValue(r.Context(), authInfoKey, authInfo)
        
        // 将新上下文传递给下一个处理程序
        next.ServeHTTP(w,r.WithContext(newCtx))
    })
}
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

身份验证中间件创建一个*AuthInfo实例,并使用包含身份验证信息的上下文调用链中的下一个处理程序。这段代码的问题在于,*AuthInfo包含一个privileges字段,该字段在调用AuthInfo.GetPrivileges时初始化。由于上下文可以由处理程序传递给多个goroutine,这种延迟初始化方案容易引发数据竞争;多个调用AuthInfo.GetPrivileges的goroutine可能会尝试多次初始化映射,导致相互覆盖。

可以使用互斥锁(mutex)来修正这个问题:

type AuthInfo struct {
    sync.Mutex
    UserID     string
    privileges map[string]Privilege
}
func (auth *AuthInfo) GetPrivileges() map[string]Privilege {
    // 使用互斥锁以线程安全的方式初始化权限
    auth.Lock()
    defer auth.Unlock()
    
    if auth.privileges == nil {
    	auth.privileges = GetPrivileges(auth.UserID)
    }
    
    return auth.privileges
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

也可以在中间件中一次性初始化权限来修正这个问题:

authInfo, err = authenticate(r)
if err ! =nil {
    http.Error(w,err.Error(), http.StatusUnauthorized)
    return
}

// 在创建结构体时在这里初始化权限
authInfo.GetPrivileges()
1
2
3
4
5
6
7
8

# 使用上下文进行取消操作

你可能想要取消一个计算过程,原因有很多:客户端可能已经断开连接,或者你可能有多个goroutine在进行一项计算,其中一个失败了,所以你不再希望其他的继续运行。你可以使用其他方法,比如关闭一个done通道来发出取消信号,但根据具体的使用场景,上下文可能会更方便。上下文可以被多次取消(实际上只有第一次调用会执行取消操作,其余的调用将被忽略),而你不能关闭一个已经关闭的通道,否则会导致程序 panic。此外,你可以创建一个上下文树,取消一个上下文只会取消由它控制的goroutine,而不会影响其他的goroutine。

# 操作方法……

创建一个可取消的上下文并检测取消操作的步骤如下:

  1. 使用context.WithCancel基于现有上下文创建一个新的可取消上下文和一个取消函数(下列代码中的cancle函数):
ctx := context.Background()
cancelable, cancel := context.WithCancel(ctx)
defer cancel()
1
2
3

确保最终调用取消函数。取消操作会释放与该上下文相关的资源。 2. 将可取消上下文传递给可取消的计算任务或goroutine:

go cancelableGoroutine1(cancelable)
go cancelableGoroutine2(cancelable)
cancelableFunc(cancelable)
1
2
3
  1. 在可取消函数中,使用ctx.Done()通道或ctx.Err()检查上下文是否已被取消:
func cancelableFunc(ctx context.Context) {
    // 处理一些数据
    // 检查上下文是否取消
    select {
    case <-ctx.Done():
        // 上下文已取消
        return
        
    default:
    }
    
    // 继续计算
}
1
2
3
4
5
6
7
8
9
10
11
12
13

或者,使用以下方式:

func cancelableFunc(ctx context.Context) {
    // 处理一些数据
    // 检查上下文是否取消
    if ctx.Err() != nil {
        // 上下文已取消
        return
    }
    
    // 继续计算
}
1
2
3
4
5
6
7
8
9
10
  1. 要手动取消一个函数,调用取消函数:
ctx := context.Background()
cancelable, cancel := context.WithCancel(ctx)
defer cancel()

wg := sync.WaitGroup{}
wg.Add(1)
go cancelableGoroutine1(cancelable, &wg)

if err := process(ctx); err != nil {
    // 取消上下文
    cancel()
    
    // 执行其他操作
}

wg.Wait()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. 确保最终调用取消函数(使用defer cancel()):
cancelable, cancel := context.WithCancel(ctx)
defer cancel()
...
1
2
3

警告

确保调用取消函数非常重要。如果你不取消可取消的上下文,与该上下文关联的goroutine将会泄漏(即,没有办法终止这些goroutine,它们会持续消耗内存)。

提示
取消函数可以多次调用,后续的调用将被忽略。

# 工作原理……

context.WithCancel返回一个新的上下文和取消闭包。返回的上下文是基于原始上下文的可取消上下文:

// 空上下文,不可取消
originalContext := context.Background()
// 基于originalContext的可取消上下文
cancelableContext1, cancel1 := context.WithCancel(originalContext)
1
2
3
4

你可以使用这个上下文来控制多个goroutine:

go f1(cancelableContext1)
go f2(cancelableContext1)
1
2

你还可以基于一个可取消上下文创建其他可取消上下文:

cancelableContext2, cancel2 := context.WithCancel(cancelableContext1)
go g1(cancelableContext2)
go g2(cancelableContext2)
1
2
3

现在,我们有两个可取消上下文。调用cancel2只会取消cancelableContext2:

cancel2() // 仅取消g1和g2
1

调用cancel1将取消cancelableContext1和cancelableContext2:

cancel1() // 取消f1、f2、g1、g2
1

上下文取消并不是自动取消goroutine的方式。你必须检查上下文是否取消并相应地进行清理:

func f1(cancelableContext context.Context) {
    for {
        if cancelableContext.Err() != nil {
            // 上下文已取消
            // 清理并返回
            return
        }
        
        // 处理数据
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 使用上下文设置超时

超时实际上就是自动取消。在定时器到期后,上下文将被取消。这对于限制不太可能完成的计算任务的资源消耗很有用。

# 操作方法……

创建一个带超时的上下文并检测超时事件发生的步骤如下:

  1. 使用context.WithTimeout基于现有上下文创建一个新的可取消上下文,该上下文会在给定的持续时间后自动取消,并返回一个取消函数:
ctx := context.Background()
timeoutable, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
1
2
3

或者,你可以使用WithDeadline在给定时刻取消上下文。确保最终调用取消函数。 2. 将带超时的上下文传递给可能会超时的计算任务或goroutine:

go longRunningGoroutine1(timeoutable)
go longRunningGoroutine2(timeoutable)
1
2
  1. 在goroutine中,使用ctx.Done()通道或ctx.Err()检查上下文是否已被取消:
func longRunningGoroutine(ctx context.Context) {
    // 处理一些数据
    // 检查上下文是否取消
    select {
    case <-ctx.Done():
        // 上下文已取消
        return
    default:
    }
    
    // 继续计算
}
1
2
3
4
5
6
7
8
9
10
11
12

或者,使用以下方式:

func cancelableFunc(ctx context.Context) {
    // 处理一些数据
    // 检查上下文是否取消
    if ctx.Err() != nil {
        // 上下文已取消
        return
    }
    
    // 继续计算
}
1
2
3
4
5
6
7
8
9
10
  1. 要手动取消一个函数,调用取消函数:
ctx := context.Background()
timeoutable, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

wg := sync.WaitGroup{}
wg.Add(1)
go longRunningGoroutine(timeoutable, &wg)
if err := process(ctx); err != nil {
    // 取消上下文
    cancel()
    // 执行其他操作
}

wg.Wait()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 确保最终调用取消函数(使用defer cancel()):
timeoutable, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
...
1
2
3

# 工作原理……

超时功能实际上就是附加了一个定时器的取消操作。当定时器到期时,上下文就会被取消。

# 更多内容……

可能会出现这样的情况:一个goroutine阻塞了,却没有明显的方法来取消它。例如,你可能会阻塞等待从网络连接中读取数据:

func readData(conn net.Conn) {
    // 从连接中读取一块数据
    msg := make([]byte, 1024)
    n, err := conn.Read(msg)
   ...
}
1
2
3
4
5
6

这个操作无法取消,因为Read方法不接受Context。如果你想取消这样的操作,可以异步关闭底层的连接(或文件)。以下代码片段展示了一个用例:必须在一秒内读取完连接中的所有数据,否则一个goroutine将异步关闭连接:

timeout, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
// 当上下文超时时关闭连接
go func() {
    // 等待取消信号
    <-timeout.Done()
    // 关闭连接
    conn.Close()
}()

wg := sync.WaitGroup()
wg.Add(1)
// 这个goroutine必须在一秒内完成,否则连接将被关闭
go func() {
    defer wg.Done()
    // 从连接中读取一块数据
    msg := make([]byte, 1024)
    // 这个调用可能会阻塞
    n, err := conn.Read(msg)
    if err != nil {
        return
    }
    // 处理数据
}()

wg.Wait() // 等待连接处理完成
...
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

# 在服务器中使用取消和超时机制

网络服务器通常在收到新请求时启动一个新的上下文。通常,当请求者关闭连接时,服务器会取消该上下文。包括标准库在内的大多数HTTP框架都遵循这个基本模式。如果你正在编写自己的TCP服务器,则必须自己实现这一机制。

# 操作方法……

处理带超时或取消机制的网络连接的步骤如下:

  1. 当接受一个网络连接时,创建一个带取消或超时的新上下文。
  2. 确保上下文最终被取消。
  3. 将上下文传递给处理函数:
ln, err := net.Listen("tcp", ":8080")
if err != nil {
    return err
}

for {
    conn, err := ln.Accept()
    if err != nil {
        return err
    }
    
    go func(c net.Conn) {
        // 步骤1:
        // 请求在RequestTimeout时长后超时
        ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)
        // 步骤2:
        // 确保调用取消函数
        defer cancel()
        // 步骤3:
        // 将上下文传递给处理函数
        handleRequest(ctx, c)
    }(conn)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 推荐阅读

讲解go context用法非常好的文档:

https://www.sohamkamani.com/golang/context/ (opens new window)

第8章 错误与恐慌(panic)
第10章 处理大量数据

← 第8章 错误与恐慌(panic) 第10章 处理大量数据→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式