一文了解 Go 中的 Context 实践

简介: Hello folks,今天我们介绍一下 Golang 语言中 Context 包相关概念特性及编程实践,这里我们以最新的 v1.18.2 为例。


“软件包上下文定义了上下文类型,它跨 API 边界和进程之间携带截止日期、取消信号和其他请求范围的值。”- golang.org

   

    Hello folks,今天我们介绍一下 Golang 语言中 Context 包相关概念特性及编程实践,这里我们以最新的 v1.18.2 为例。

    Context 是 Golang 标准库中的一个核心包,其提供了 goroutine 之间的传递信息的机制、信号同步,除此之外,其还具备超时( Timeout )和取消( Cancel )机制。总而言之,一句话总结:Context 控制子 goroutine 的运行,超时控制及取消的方法调用。

    在解析 Context 之前,让我们先来看个 Demo,在下面的示例中,我们模拟一个简单的 HTTP 服务器来处理请求,具体如下所示:


[leonli@192 context ] % vi main.go
package main
import (
    "fmt"
    "net/http"
    "time"
)
func demoRequest(w http.ResponseWriter, req *http.Request) {
    fmt.Println("Hi girl, I love you !")
    context := req.Context()
    select {
    // Simulating some work by the server, waits 5 seconds and then responds.
    case <-time.After(5 * time.Second):
        fmt.Fprintf(w, "Hi man, I love you too!")
    // Handling request cancellation
    case <-context.Done():
        err := context.Err()
        fmt.Println("Error:", err)
    }
    fmt.Println("Let's go !")
}
func main() {
    http.HandleFunc("/request", demoRequest)
    fmt.Println("Confession is running...")
    http.ListenAndServe(":4000", nil)
}

    此时,我们打开 2 个窗体,在窗体 1 中,运行 Go 程序,具体如下:


[leonli@192 context ] % go run main.go 
Confession is running...

    然后,我们跳转至窗体 2 运行客户端请求,如下所示:


[leonli@192 context ] % curl localhost:4000/request

    此时,我们查看窗体 1 ,发现输出:Hi girl, I love you !” ,具体如下:


[leonli@192 context ] % go run main.go
Confession is running...
Hi girl, I love you !

    等待数秒后,窗体 2 输出Hi man, I love you too! ” ,如下所示:


[leonli@192 context ] % curl localhost:4000/request
Hi man, I love you too!

    然后,再一次回到窗体 1 ,我们将会看到如下:


[leonli@192 context ] % go run main.go
Confession is running...
Hi girl, I love you !
Let's go !

    在 Golang 中,传入服务器的请求应创建上下文,向服务器发出的调用应接受上下文。它们之间的函数调用链基于上下文传播,可以选择将其替换为使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 创建的派生上下文。当上下文被取消时,从中派生的所有上下文也会被取消。

    WithCancel、WithDeadline 和 WithTimeout 函数采用 Context(父)并返回派生 Context(子)和 aCancelFunc。调用 CancelFunc 将取消子项及其子项,删除父项对父项的引用,并停止任何关联的计时器。未能调用 CancelFunc 会泄露子及其子项,直到父项被取消或计时器触发。

    从本质上讲,Context 是一种 Interface 类型,其结构定义如下:


type Context interface {
  Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{}
  Err() error
  Value(key any) any
}

    具体来说,Context 类型通常具有如下方法:

    1、Done() <- chan struct{} 返回上下文取消或超时时关闭的通道。若上下文永远无法取消,则完成可能会返回 nil。

    2、Deadline() (deadline time.Time, ok bool) 返回上下文被取消或超时的时间。当没有设定截止日期时,截止日期返回为 false。

    3、Err() error 返回一个错误,即解释了 Done 通道被关闭的原因。如果尚未关闭,它将返回 nil。

    4、Value(key any) any 返回与键关联的值,若无,则返回 nil。

   接下来,我们简要描述下 Context 所包含的功能特性。

func WithValue


func WithValue(parent Context, key, val any) Context

    此函数接受上下文并返回派生上下文,其中值 val 与 key 相关联,并与上下文一起流经上下文树。

    这意味着,一旦获得具有值的上下文,由此派生的任何上下文都会获得此值。通常而言,不建议使用上下文值传递关键参数,相反,函数应该在签名中接受这些显式值。

    如下示例演示了如何将值传递到上下文,以及如果存在该值,如何检索它。


[leonli@192 context ] % vi main-withvalue.go
package main
import (
        "context"
        "fmt"
)
func main() {
        type favContextKey string
        f := func(ctx context.Context, k favContextKey) {
                if v := ctx.Value(k); v != nil {
                        fmt.Println("found value:", v)
                        return
                }
                fmt.Println("key not found:", k)
        }
        k := favContextKey("language")
        ctx := context.WithValue(context.Background(), k, "9527")
        f(ctx, k)
        f(ctx, favContextKey("That guy is not here !"))
}
Output:
found value: 9527
key not found: That guy is not here !

func WithCancel


func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

    此函数从父上下文和派生上下文以及取消函数创建新上下文。父级可以是 context.Background 或传递到函数中的上下文。

    取消此上下文会释放与之关联的资源,因此代码应在此上下文中运行的操作完成后立即调用取消。通常情况下,不建议传递 Cancel 功能,因为它可能会导致意外行为。如下示例演示了使用可取消上下文来防止程序泄漏。具体可参考如下示例:


[leonli@192 context ] % vi main-withcancel.go
package main
import (
    "runtime"
    "fmt"
    "time"
    "context"
)
func thread2(ctx context.Context, number int) {
    for {
        select {
        case v := <- ctx.Done():
            fmt.Printf("thread: %v, the received channel value is: %v, ending\n", number,v)
            return
        default:
            fmt.Printf("thread: %v here!\n", number)
            time.Sleep(2 * time.Second)
        }
    }
}
func thread1(ctx context.Context, number int) {
    for {
        go thread2(ctx, number)
        select {
        case v := <- ctx.Done():
            fmt.Printf("thread: %v, too far to go: %v, ending\n", number, v)
            return
        default:
            fmt.Printf("thread: %v here!\n", number)
            time.Sleep(2 * time.Second)
        }
    }
}
func main() {
    var ctx context.Context = nil
    var cancel context.CancelFunc = nil
    ctx, cancel = context.WithCancel(context.Background())
    for i := 1; i <= 5; i = i + 1 {
        go thread1(ctx, i)
    }
    time.Sleep(1 * time.Second)
    // close all gourtines
    cancel()
    // waiting 10 seconds, if the screen does not display <thread: xxxx in progress>, all goroutines have been shut down
    time.Sleep(10 * time.Second)
    println(runtime.NumGoroutine())
    println("Fucking, stop playing!")
}
Output:
thread: 2 here!
thread: 1 here!
thread: 3 here!
thread: 2 here!
thread: 5 here!
thread: 3 here!
thread: 1 here!
thread: 5 here!
thread: 4 here!
thread: 4 here!
thread: 5, too far to go: {}, ending
thread: 4, too far to go: {}, ending
thread: 5, too far to go: {}, ending
thread: 3, too far to go: {}, ending
thread: 4, too far to go: {}, ending
thread: 3, too far to go: {}, ending
thread: 4, too far to go: {}, ending
thread: 1, too far to go: {}, ending
thread: 5, too far to go: {}, ending
thread: 1, too far to go: {}, ending
thread: 2, too far to go: {}, ending
thread: 2, too far to go: {}, ending
thread: 2, too far to go: {}, ending
thread: 1, too far to go: {}, ending
thread: 3, too far to go: {}, ending
1
Fucking, stop playing!

func WithDeadline


func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

    此函数返回其父函数的派生上下文,当截止日期超过或调用取消函数时,该上下文会被取消。

    例如,我们可以创建一个上下文,该上下文将在未来某个时间自动被取消,并将其传递给子函数。当该上下文因截止日期即将到期而被取消时,所有获得上下文的功能都会收到停止工作并返回的通知。具体可参考如下:


[leonli@192 context ] % vi main-withdeadline.go
package main
import (
    "runtime"
    "fmt"
    "time"
    "context"
)
func thread2(ctx context.Context, index int) {
    for {
        select {
        case v := <- ctx.Done():
            fmt.Printf("thread2: %v, too far to go: %v, ending\n", index, v)
            return
        default:
            fmt.Printf("thread2: %v here\n", index)
            time.Sleep(2 * time.Second)
        }
    }
}
func thread1(ctx context.Context, index int) {
    for {
        go thread2(ctx, index)
        select {
        case v := <- ctx.Done():
            // this branch is only reached when the ch channel is closed, or when data is sent(either true or false)
            fmt.Printf("thread1: %v, too far to go: %v, ending\n", index, v)
            return
        default:
            fmt.Printf("thread1: %v here\n", index)
            time.Sleep(2 * time.Second)
        }
    }
}
func main() {
    var ctx01 context.Context = nil
    var ctx02 context.Context = nil
    var cancel context.CancelFunc = nil
    ctx01, cancel = context.WithCancel(context.Background())
    ctx02, cancel = context.WithDeadline(ctx01, time.Now().Add(1 * time.Second)) // If it's WithTimeout, just change this line to "ctx02, cancel = context.WithTimeout(ctx01, 1 * time.Second)"
    defer cancel()
    for i := 1; i <= 5; i = i + 1 {
        go thread1(ctx02, i)
    }
    time.Sleep(5 * time.Second)
    if ctx02.Err() != nil {
        fmt.Println("the cause of cancel is: ", ctx02.Err())
    }
    println(runtime.NumGoroutine())
    println("Fucking, stop playing!")
}
Output:
thread1: 1 here
thread1: 5 here
thread1: 2 here
thread1: 4 here
thread2: 5 here
thread2: 1 here
thread2: 4 here
thread2: 2 here
thread1: 3 here
thread2: 3 here
thread1: 3, too far to go: {}, ending
thread1: 4, too far to go: {}, ending
thread2: 3, too far to go: {}, ending
thread2: 3, too far to go: {}, ending
thread2: 2, too far to go: {}, ending
thread1: 1, too far to go: {}, ending
thread2: 5, too far to go: {}, ending
thread2: 1, too far to go: {}, ending
thread2: 4, too far to go: {}, ending
thread1: 5, too far to go: {}, ending
thread2: 5, too far to go: {}, ending
thread2: 4, too far to go: {}, ending
thread2: 1, too far to go: {}, ending
thread1: 2, too far to go: {}, ending
thread2: 2, too far to go: {}, ending
the cause of cancel is:  context deadline exceeded
1
Fucking, stop playing!

func WithTimeout


func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
  return WithDeadline(parent, time.Now().Add(timeout))
}

    此函数通常只是带有额外超时的 WithDeadline 函数的包装器。具体功能实现可参考如上代码所示。

    至此,针对 Context 的相关功能解析及实践 Demo 先解析到此为止,希望大家有所收获!

    Adiós !

相关文章
|
30天前
|
Go 调度 开发者
Go语言中的并发编程:深入理解与实践###
探索Go语言在并发编程中的独特优势,揭秘其高效实现的底层机制。本文通过实例和分析,引导读者从基础到进阶,掌握Goroutines、Channels等核心概念,提升并发处理能力。 ###
|
11天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
1月前
|
Cloud Native Go API
Go语言在微服务架构中的创新应用与实践
本文深入探讨了Go语言在构建高效、可扩展的微服务架构中的应用。Go语言以其轻量级协程(goroutine)和强大的并发处理能力,成为微服务开发的首选语言之一。通过实际案例分析,本文展示了如何利用Go语言的特性优化微服务的设计与实现,提高系统的响应速度和稳定性。文章还讨论了Go语言在微服务生态中的角色,以及面临的挑战和未来发展趋势。
|
1月前
|
Go 开发者
Go语言中的并发编程:从基础到实践
在当今的软件开发中,并发编程已经成为了一项不可或缺的技能。Go语言以其简洁的语法和强大的并发支持,成为了开发者们的首选。本文将带你深入了解Go语言中的并发编程,从基础概念到实际应用,帮助你掌握这一重要的编程技能。
|
1月前
|
SQL 关系型数据库 MySQL
Go语言项目高效对接SQL数据库:实践技巧与方法
在Go语言项目中,与SQL数据库进行对接是一项基础且重要的任务
59 11
|
18天前
|
NoSQL 测试技术 Go
自动化测试在 Go 开源库中的应用与实践
本文介绍了 Go 语言的自动化测试及其在 `go mongox` 库中的实践。Go 语言通过 `testing` 库和 `go test` 命令提供了简洁高效的测试框架,支持单元测试、集成测试和基准测试。`go mongox` 库通过单元测试和集成测试确保与 MongoDB 交互的正确性和稳定性,使用 Docker Compose 快速搭建测试环境。文章还探讨了表驱动测试、覆盖率检查和 Mock 工具的使用,强调了自动化测试在开源库中的重要性。
|
20天前
|
Go
Go语言中的并发编程:深入探索与实践###
探索Go语言的并发编程,就像解锁了一把高效处理复杂任务的钥匙。本文旨在通过简明扼要的方式,阐述Goroutines和Channels如何协同工作,以实现高效的并发处理。不同于传统的技术文档,这里我们将用一个生动的故事来串联起这些概念,让你在轻松阅读中领悟到并发编程的精髓。 ###
|
1月前
|
存储 Go 数据库
Go语言Context包源码学习
【10月更文挑战第21天】Go 语言中的 `context` 包用于在函数调用链中传递请求上下文信息,支持请求的取消、超时和截止时间管理。其核心接口 `Context` 定义了 `Deadline`、`Done`、`Err` 和 `Value` 方法,分别用于处理截止时间、取消信号、错误信息和键值对数据。包内提供了 `emptyCtx`、`cancelCtx`、`timerCtx` 和 `valueCtx` 四种实现类型,满足不同场景需求。示例代码展示了如何使用带有超时功能的上下文进行任务管理和取消。
|
1月前
|
消息中间件 监控 Go
Go语言在微服务架构中的优势与实践
【10月更文挑战第10天】Go语言在微服务架构中的优势与实践
|
1月前
|
Java Go 数据库
Go语言在微服务架构中的优势与实践
【10月更文挑战第10天】Go语言在微服务架构中的优势与实践