第 17 章 - Go语言 上下文( Context )

news2024/11/18 16:26:45

在Go语言中,context包为跨API和进程边界传播截止时间、取消信号和其他请求范围值提供了一种方式。它主要应用于网络服务器和长时间运行的后台任务中,用于控制一组goroutine的生命周期。下面我们将详细介绍context的定义、使用场景、取消和超时机制,并通过案例和源码解析来加深理解。

Context的定义

context.Context接口定义如下:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline()返回一个时间点,表示这个请求的截止时间。如果返回的okfalse,则没有设置截止时间。
  • Done()返回一个通道,当请求应该被取消时,这个通道会关闭。通常用于监听取消或超时事件。
  • Err()返回导致Done通道关闭的原因。如果Done尚未关闭,则返回nil
  • Value()用于传递请求范围内的数据,如用户身份验证信息等。它不应该用于传递可变状态。

使用场景

context主要用于以下场景:

  • 当处理HTTP请求时,可以将请求的上下文信息传递给处理函数及其调用的所有子goroutine。
  • 在长时间运行的任务中,可以通过context来传递取消信号,以便优雅地终止任务。
  • 当需要设置操作的超时时,可以使用带有超时功能的context

取消和超时

取消

取消context可以通过创建一个可取消的context实现,例如使用context.WithCancel函数:

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保在函数退出时调用cancel

调用cancel()函数后,ctx.Done()返回的通道会被关闭,所有监听该通道的goroutine都会收到取消信号。

超时

对于需要设置超时的情况,可以使用context.WithTimeout

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

在这个例子中,5秒后ctx.Done()通道会自动关闭,发送超时信号。

案例

假设我们有一个HTTP服务器,需要处理每个请求的最大时间为2秒,超过这个时间就取消请求:

func handler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Fprintf(w, "Request processed successfully")
    case <-ctx.Done():
        fmt.Fprintf(w, "Request processing timed out: %v", ctx.Err())
    }
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

源码分析

context包中的核心是几个实现了Context接口的结构体,如cancelCtxtimerCtx等。以timerCtx为例,它是select语句中的定时器实现的基础,用于处理超时情况。timerCtx内部维护了一个time.Timer对象,当超时发生时,会关闭Done通道。

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Underlying timer.
    // ...
}

// WithTimeout returns WithCancel(parent, Background()) and starts a Timer running.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    if timeout <= 0 {
        return WithDeadline(parent, time.Time{})
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  time.Now().Add(timeout),
    }
    // ...
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(timeout, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, c.Cancel
}

以上就是关于Go语言context的基本介绍、使用场景、取消和超时机制的详解,以及简单的案例和源码分析。希望这些信息能够帮助你更好地理解和使用context

当然,我们可以进一步深入探讨context的高级使用技巧、最佳实践以及一些常见的误区。

高级使用技巧

值传递

context允许通过Value方法传递请求特定的数据。这是一种轻量级的机制,用于在请求的处理链中传递信息,比如用户认证信息、请求ID等。但是需要注意的是,Value应该仅用于传递请求作用域的数据,而不是作为全局变量的替代品。

示例代码:

type key int

const (
    userKey key = 0
)

func middleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        user := getUserFromDatabase(r) // 假设从数据库获取用户信息
        ctx := context.WithValue(r.Context(), userKey, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    }
}

func handler(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value(userKey)
    if user != nil {
        fmt.Fprintf(w, "Hello, %s!", user.(string))
    } else {
        http.Error(w, "User not found", http.StatusUnauthorized)
    }
}
多个context的组合

有时候你可能需要组合多个context,例如同时设置超时和取消。这可以通过嵌套调用WithCancelWithTimeout来实现。

示例代码:

parentCtx := context.Background()
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()

ctx, cancel = context.WithCancel(ctx)
defer cancel()

最佳实践

  1. 避免过度使用Value:虽然Value方法非常方便,但过度使用可能会导致代码难以维护。应该只传递真正需要的信息。

  2. 及时释放资源:使用defer确保cancel函数总是被调用,这样可以避免资源泄露。

  3. 不要在context中存储大量数据context中的数据应该是轻量级的,避免存储大对象。

  4. 避免直接使用context.Backgroundcontext.TODO:在实际应用中,应该根据具体需求创建适当的context

常见误区

  1. 误用context.TODOcontext.TODO是一个占位符,用于表示将来会提供一个合适的context。在生产代码中,应该使用具体的context,而不是TODO

  2. 忽略Done通道:在处理长时间运行的任务时,应该始终监听Done通道,以便在接收到取消信号时能够及时停止任务。

  3. 错误地传递contextcontext应该从请求的入口点传递到所有需要它的组件,而不仅仅是部分组件。

源码深入

让我们更深入地看看context的一些内部实现细节。以cancelCtx为例,这是context中最基本的可取消类型。

type cancelCtx struct {
    Context
    mu         sync.Mutex            // protects following fields
    done       atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
    children   map[canceler]struct{} // set to nil by the first cancel call
    err        error                 // set to non-nil by the first cancel call
}

func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done.Load().(chan struct{})
    c.mu.Unlock()
    return d
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return
    }
    c.err = err
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done.Load().(chan struct{})) // ignore multiple closes
    }
    for child := range c.children {
        // Notify child that it has been canceled.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}
  • Done方法返回一个通道,该通道在cancel被调用时关闭。
  • Err方法返回导致Done通道关闭的错误。
  • cancel方法用于关闭Done通道并通知所有子context

通过上述源码,我们可以看到context是如何管理其生命周期和子context的。

总结

context是Go语言中处理并发、超时和取消信号的重要工具。正确使用context可以使你的程序更加健壮和高效。

我们可以进一步探讨一些更高级的主题,包括如何在实际项目中更好地使用context,以及一些常见问题的解决方案。

实际项目中的最佳实践

1. 统一处理取消和超时

在实际项目中,经常会遇到需要同时处理取消和超时的情况。可以通过组合WithTimeoutWithCancel来实现这一点。

func processRequest(ctx context.Context) error {
    // 创建一个带超时的上下文
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    // 执行耗时操作
    select {
    case <-time.After(3 * time.Second):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}
2. 在中间件中使用context

在Web框架中,中间件是处理请求的常用模式。通过在中间件中传递context,可以确保每个请求的上下文信息在整个处理链中都能被访问到。

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        ctx := context.WithValue(r.Context(), "startTime", start)
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
        log.Printf("Request took %v", time.Since(start))
    })
}

func handler(w http.ResponseWriter, r *http.Request) {
    startTime := r.Context().Value("startTime").(time.Time)
    // 处理请求
    time.Sleep(2 * time.Second)
    w.Write([]byte(fmt.Sprintf("Request started at %v", startTime)))
}

func main() {
    http.Handle("/", loggingMiddleware(http.HandlerFunc(handler)))
    http.ListenAndServe(":8080", nil)
}
3. 在数据库操作中使用context

在进行数据库操作时,使用context可以确保长时间运行的查询在必要时能够被取消。

func getUserByID(ctx context.Context, db *sql.DB, id int) (*User, error) {
    var user User
    query := "SELECT id, name, email FROM users WHERE id = $1"
    row := db.QueryRowContext(ctx, query, id)
    err := row.Scan(&user.ID, &user.Name, &user.Email)
    if err != nil {
        return nil, err
    }
    return &user, nil
}

常见问题及解决方案

1. 忘记调用cancel函数

忘记调用cancel函数会导致资源泄漏。确保在每次创建context时都使用defer来调用cancel函数。

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
2. context中的值类型不一致

在使用context.Value时,确保传递和接收的值类型一致。可以通过定义常量或类型来避免类型错误。

type key int

const userKey key = 0

func handler(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value(userKey)
    if user == nil {
        http.Error(w, "User not found", http.StatusUnauthorized)
        return
    }
    fmt.Fprintf(w, "Hello, %s!", user.(string))
}
3. context的传递深度过深

在复杂的系统中,context的传递深度可能会很深。为了避免代码复杂性,可以考虑使用中间件或封装函数来简化context的传递。

func withContext(ctx context.Context, fn func(context.Context) error) error {
    return fn(ctx)
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := withContext(ctx, func(ctx context.Context) error {
        // 执行操作
        return nil
    }); err != nil {
        log.Println("Error:", err)
    }
}

进阶主题

1. 自定义context类型

在某些情况下,你可能需要自定义context类型以满足特定需求。可以通过继承context.Context接口来实现。

type customContext struct {
    context.Context
    customData string
}

func NewCustomContext(parent context.Context, data string) context.Context {
    return &customContext{parent, data}
}

func (c *customContext) CustomData() string {
    return c.customData
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := NewCustomContext(r.Context(), "some custom data")
    // 使用自定义上下文
    fmt.Fprintf(w, "Custom data: %s", ctx.(*customContext).CustomData())
}
2. context的性能优化

在高并发场景下,频繁创建和销毁context可能会带来性能开销。可以通过复用context或使用池化技术来优化性能。

var contextPool = sync.Pool{
    New: func() interface{} {
        return context.WithValue(context.Background(), "key", "value")
    },
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := contextPool.Get().(context.Context)
    defer contextPool.Put(ctx)

    // 使用复用的上下文
    fmt.Fprintf(w, "Using pooled context")
}

总结

通过上述内容,我们进一步探讨了context在实际项目中的最佳实践、常见问题及解决方案,以及一些进阶主题。希望这些内容能帮助你在实际开发中更好地利用context,提高代码的健壮性和可维护性。希望这些详细的解释和示例能帮助你更好地理解和使用context

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2242910.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++11(五)----lambda表达式

文章目录 lambda表达式 lambda表达式 lambda表达式可以看作一个匿名函数 语法 [capture-list] (parameters) mutable -> return-type { statement } auto func1 [](int a, int b) mutable -> int {return a b; }; *capture-list&#xff1a;捕捉列表。编译器根据[]来 判…

CSS基础知识05(弹性盒子、布局详解,动画,3D转换,calc)

目录 0、弹性盒子、布局 0.1.弹性盒子的基本概念 0.2.弹性盒子的主轴和交叉轴 0.3.弹性盒子的属性 flex-direction row row-reverse column column-reverse flex-wrap nowrap wrap wrap-reverse flex-dirction和flex-wrap的组合简写模式 justify-content flex-s…

【Qt聊天室】客户端实现总结

目录 1. 项目概述 2. 功能实现 2.1 主窗口设计 2.2 功能性窗口 2.3 主界面功能实现 2.4 聊天界面功能实现 2.5 个人信息功能开发 2.6 用户信息界面设置功能 2.7 单聊与群聊 2.8 登录窗口 2.9 消息功能 3. 核心设计逻辑 3.1 核心类 3.2 前后端交互与DataCenter 4…

RK3568平台开发系列讲解(高级字符设备篇)IO 模型引入实验

🚀返回专栏总目录 文章目录 一、IO 的概念二、IO 执行过程三、IO 模型的分类阻塞 IO非阻塞 IOIO 多路复用信号驱动异步 IO沉淀、分享、成长,让自己和他人都能有所收获!😄 一、IO 的概念 IO 是英文 Input 和 Output 的首字母, 代表了输入和输出, 当然这样的描述有一点点…

简单实现QT对象的[json]序列化与反序列化

简单实现QT对象的[json]序列化与反序列化 简介应用场景qt元对象系统思路实现使用方式题外话 简介 众所周知json作为一种轻量级的数据交换格式&#xff0c;在开发中被广泛应用。因此如何方便的将对象数据转为json格式和从json格式中加载数据到对象中就变得尤为重要。 在python类…

【qt】控件2

1.frameGeometry和Geometry区别 frameGeometry是开始从红圈开始算&#xff0c;Geometry从黑圈算 程序证明&#xff1a;使用一个按键&#xff0c;当按键按下,qdebug打印各自左上角的坐标&#xff08;相当于屏幕左上角&#xff09;&#xff0c;以及窗口大小 Widget::Widget(QWid…

LeetCode654.最大二叉树

LeetCode刷题记录 文章目录 &#x1f4dc;题目描述&#x1f4a1;解题思路⌨C代码 &#x1f4dc;题目描述 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。 递归地在最大值 左边 的 子…

华为欧拉系统使用U盘制作引导安装华为欧拉操作系统

今天记录一下通过U盘来安装华为欧拉操作系统 华为欧拉操作系统是国产的一个类似于Centos的Linus系统 具体实现操作步骤&#xff1a; 先在官网下载欧拉系统镜像点击跳转到下载 准备好一个大于16g的U盘 &#xff0c;用于制作U盘启动 下载一个引导程序制作工具&#xff0c;我使用…

软考教材重点内容 信息安全工程师 第 3 章 密码学基本理论

&#xff08;本章相对老版本极大的简化&#xff0c;所有与算法相关的计算全部删除&#xff0c;因此考试需要了解各个常 用算法的基本参数以及考试中可能存在的古典密码算法的计算&#xff0c;典型的例子是 2021 和 2022 年分别考了 DES 算法中的 S 盒计算&#xff0c;RSA 中的已…

如何让手机ip变成动态

在数字化浪潮中&#xff0c;手机已成为我们日常生活中不可或缺的一部分。无论是浏览网页、使用社交媒体还是进行在线购物&#xff0c;手机都扮演着举足轻重的角色。然而&#xff0c;在享受网络带来的便利时&#xff0c;我们也需要关注网络安全和隐私保护。静态IP地址可能让手机…

64位程序调用32位dll解决方案

最近在做64位代码移植&#xff0c;发现很多老代码使用到了第三方的32位dll;而且这些第三方32位dll库已经年代久远&#xff0c;原开发商已不再了&#xff1b;所以急切的需要在64位主程序 中使用老的32位dll;查询很多解决方案 发现目前只有使用com 进程外组件的方法可以解决此问题…

无人机挂载超细干粉灭火装置技术详解

无人机挂载超细干粉灭火装置技术是一种创新的灭火方式&#xff0c;结合了无人机的远程操控能力和超细干粉灭火剂的高效灭火性能。以下是对该技术的详细解析&#xff1a; 一、技术背景与原理 背景&#xff1a;高层建筑灭火救援困难一直是公认的世界性难题。无人机技术的发展为…

信号-3-信号处理

main 信号捕捉的操作 sigaction struct sigaction OS不允许信号处理方法进行嵌套&#xff1a;某一个信号正在被处理时&#xff0c;OS会自动block改信号&#xff0c;之后会自动恢复 同理&#xff0c;sigaction.sa_mask 为捕捉指定信号后临时屏蔽的表 pending什么时候清零&…

Linux的指令(三)

1.grep指令 功能&#xff1a; 在文件中搜索字符串&#xff0c;将找到的行打印出来 -i&#xff1a;忽略大小写的不同&#xff0c;所以大小写视为一样 -n&#xff1a;顺便输出行号 -v:反向选择&#xff0c;就是显示出没有你输入要搜索内容的内容 代码示例&#xff1a; roo…

onvif协议相关:4.1.7 Digest方式云台控制停止

背景 关于onvif的其实很早之前我已经在专栏中写了不少了, 使用onvif协议操作设备 但最近有陆陆续续的粉丝问我, 希望我在写一些关于 onvif的设备自动发现、预置位跳转、云台操作的博客。 满足粉丝的需求,安排。 今天我们来实现 设备云台的控制(启动) 实现 1.在ONVIF Devi…

【机器学习】数学知识:标准差,方差,协方差,平均数,中位数,众数

标准差、方差和协方差是统计学中重要的概念&#xff0c;用于描述数据的分散程度和变量之间的关系。以下是它们的定义和公式&#xff1a; 1. 标准差 (Standard Deviation) 标准差是方差的平方根&#xff0c;表示数据的分散程度&#xff0c;以与数据相同的单位表示。 公式&…

数据结构习题——有效的括号(栈),栈与队列和互相实现,循环队列的实现

文章目录 前言1、有效的括号题目思路代码 2、用队列实现栈题目思路代码 3、用栈实现对列题目思路代码 4、设计循环队列4.1循环队列的概念和了解题目思路代码 总结 前言 继上篇博客学习了栈与队列之后&#xff0c;今天我们来尝试着使用他们来写一些题目&#xff0c;话不多说&…

Java连接MySQL(测试build path功能)

Java连接MySQL&#xff08;测试build path功能&#xff09; 实验说明下载MySQL的驱动jar包连接测试的Java代码 实验说明 要测试该情况&#xff0c;需要先安装好MySQL的环境&#xff0c;其实也可以通过测试最后提示的输出来判断build path是否成功&#xff0c;因为如果不成功会直…

计算机组成原理——高速缓存

标记表示——主存块号和缓存块之前的一一对应关系

Java面试之多线程并发篇(5)

前言 本来想着给自己放松一下&#xff0c;刷刷博客&#xff0c;突然被几道面试题难倒&#xff01;常用的线程池有哪些&#xff1f;简述一下你对线程池的理解&#xff1f;Java程序是如何执行的&#xff1f;锁的优化机制了解吗&#xff1f;说说进程和线程的区别&#xff1f;似乎…