一文了解 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 !

相关文章
|
2月前
|
负载均衡 算法 数据库连接
Go语言性能优化实践:案例分析与解决方案
【2月更文挑战第18天】本文将通过具体的案例分析,探讨Go语言性能优化的实践方法和解决方案。我们将分析几个典型的性能瓶颈问题,并详细介绍如何通过优化代码、调整并发模型、改进内存管理等方式来提升程序的性能。通过本文的学习,读者将能够掌握一些实用的Go语言性能优化技巧,为实际项目开发中的性能优化工作提供指导。
|
2月前
|
运维 网络协议 安全
长连接网关技术专题(十):百度基于Go的千万级统一长连接服务架构实践
本文将介绍百度基于golang实现的统一长连接服务,从统一长连接功能实现和性能优化等角度,描述了其在设计、开发和维护过程中面临的问题和挑战,并重点介绍了解决相关问题和挑战的方案和实践经验。
109 1
|
7天前
|
消息中间件 Go API
Golang深入浅出之-Go语言中的微服务架构设计与实践
【5月更文挑战第4天】本文探讨了Go语言在微服务架构中的应用,强调了单一职责、标准化API、服务自治和容错设计等原则。同时,指出了过度拆分、服务通信复杂性、数据一致性和部署复杂性等常见问题,并提出了DDD拆分、使用成熟框架、事件驱动和配置管理与CI/CD的解决方案。文中还提供了使用Gin构建HTTP服务和gRPC进行服务间通信的示例。
23 0
|
12天前
|
存储 缓存 监控
【Go语言专栏】Go语言应用的性能调优实践
【4月更文挑战第30天】本文介绍了Go语言应用的性能调优技巧,包括使用`pprof`进行性能分析、选择正确算法与数据结构、减少内存分配、优化并发及避免阻塞操作、选用合适锁机制。此外,文章还提到了编译选项如`-trimpath`和`-ldflags`,以及系统资源和环境调优。通过实例展示了代码优化、并发处理和锁的使用。最后,推荐了进一步学习资源,鼓励读者深入探索Go语言的性能优化。
|
16天前
|
Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第23天】Go语言的`context`包提供`Context`接口用于处理任务取消、超时和截止日期。通过传递`Context`对象,开发者能轻松实现复杂控制流。本文解析`context`包特性,讨论常见问题和解决方案,并给出代码示例。关键点包括:1) 确保将`Context`传递给所有相关任务;2) 根据需求选择适当的`Context`创建函数;3) 定期检查`Done()`通道以响应取消请求。正确使用`context`包能提升Go程序的控制流管理效率。
21 1
|
27天前
|
Java Go 调度
Go语言并发编程原理与实践:面试经验与必备知识点解析
【4月更文挑战第12天】本文分享了Go语言并发编程在面试中的重要性,包括必备知识点和面试经验。核心知识点涵盖Goroutines、Channels、Select、Mutex、Sync包、Context和错误处理。面试策略强调结构化回答、代码示例及实战经历。同时,解析了Goroutine与线程的区别、Channel实现生产者消费者模式、避免死锁的方法以及Context包的作用和应用场景。通过理论与实践的结合,助你成功应对Go并发编程面试。
25 3
|
2月前
|
Kubernetes Go 开发者
Go语言与Docker容器结合的实践应用与案例分析
【2月更文挑战第23天】本文通过分析实际案例,探讨了Go语言与Docker容器技术结合的实践应用。通过详细阐述Go语言在容器化环境中的开发优势,以及Docker容器技术在Go应用部署中的重要作用,本文旨在为读者提供Go语言与Docker容器结合的具体实现方法和实际应用场景。
|
2月前
|
安全 中间件 Go
Go语言Web服务性能优化与安全实践
【2月更文挑战第21天】本文将深入探讨Go语言在Web服务性能优化与安全实践方面的应用。通过介绍性能优化策略、并发编程模型以及安全加固措施,帮助读者理解并提升Go语言Web服务的性能表现与安全防护能力。
|
2月前
|
存储 算法 Go
泛型在Go语言中的引入与实践
【2月更文挑战第19天】Go语言1.18版本引入了对泛型的原生支持,这一特性使得开发者能够编写更加通用和灵活的代码。本文将深入探讨Go语言中泛型的引入背景、使用方法和实践案例,帮助读者了解并应用这一强大的编程工具。
|
2月前
|
Kubernetes Go 开发者
Go语言在容器化环境中的实践
【2月更文挑战第15天】随着容器技术的兴起,Go语言在容器化环境中的实践逐渐受到关注。本文探讨了Go语言如何与容器技术相结合,发挥其在容器化环境中的优势,包括轻量级部署、高并发处理、快速构建和部署等方面的特点,并通过实例展示了Go语言在容器化环境中的实践应用。