Go Context解读与实践

简介: Go Context解读与实践Go Context解读与实践Go Context解读与实践Go Context解读与实践

[TOC]

1 Context的初衷

In Go servers, each incoming request is handled in its own goroutine. Request handlers often start additional goroutines to access backends such as databases and RPC services. The set of goroutines working on a request typically needs access to request-specific values such as the identity of the end user, authorization tokens, and the request's deadline. When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.

如上图,很多时候,尤其是分布式架构环境中,一个请求到达服务端后,会被拆分为若干个请求转发至相关的服务单元处理,如果一个服务单元返回结束信息(通常是错误造成的),其他服务单元都应该及时结束该请求的处理,以避免资源浪费在无意义的请求处理上。

正是因于此,Google开发了context包,提供对使用一组相同上下文(context)的goroutine的管理,及时结束无意义的请求处理goroutine。

1.1 如何下发取消(结束)命令?

这就成为一个亟待解决的问题。我们都知道在Go语言中,提倡“通过通信共享内存资源”,那么下发取消命令最简单直接的办法就是创建一个结束通道(done channel),各个服务单元(goroutine)根据channel来获取结束命令。

1.2 如何根据channel来获取结束命令呢?

So easy,读值呗!有值就表示结束啊!

哈哈,事实并非如此,通道有非缓冲通道和缓冲通道,应该选择哪一种?通道中写什么值呢?是有值即结束还是根据值判断呢?

1.2.1 使用非缓冲通道

type Result struct {
    status bool
    value int
}

func thirtyAPI(done chan struct{}, num int, dst chan Result){
    fmt.Printf("我正在调用第三方API:%d\n", num)
    tc := make(chan int, 1)
    i := 0
    for {
        // 业务逻辑代码
        select {
        case <-done:
            fmt.Printf("%d: 我要结束了,Bye ThirtyAPI\n", num)
            dst <- Result{status:false, value:num}
            return
        case tc <- i:
            if num == 3 {
                time.Sleep(time.Second)
                dst <- Result{status: false, value:num}
                return
            }
            i = <-tc
            i++
        }
    }
}

func eg3() {
    dst := make(chan Result, 5)
    done := make(chan struct{})
    for i:=0; i<5; i++{
        go thirtyAPI(done, i, dst)
    }

    for result := range dst {
        if result.status == false {
            // 第一个false到来时,必须发布取消命令
            fmt.Printf("%d: I met error\n", result.value)
            done <- struct{}{}
            break
        }
    }
}

func main() {
    eg3()
    time.Sleep(time.Second*5)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

分析一下运行结果,我们发现只有一个goroutine接收到结束命令,其他的goroutine都未结束运行。这是因为代码中使用非缓冲通道造成的。

1.2.2 使用缓冲通道

type Result struct {
    status bool
    value int
}

func thirtyAPI(done chan struct{}, num int, dst chan Result){
    fmt.Printf("我正在调用第三方API:%d\n", num)
    tc := make(chan int, 1)
    i := 0
    for {
        // 业务逻辑代码
        select {
        case <-done:
            fmt.Printf("%d: 我要结束了,Bye ThirtyAPI\n", num)
            dst <- Result{status:false, value:num}
            return
        case tc <- i:
            if num == 3 {
                time.Sleep(time.Second)
                dst <- Result{status: false, value:num}
                return
            }
            if num == 4 {
                dst <- Result{status: true, value: num}
                return
            }
            i = <-tc
            i++
        }
    }
}

func eg4() {
    dst := make(chan Result, 5)
    done := make(chan struct{}, 5)
    for i:=0; i<5; i++{
        go thirtyAPI(done, i, dst)
    }

    for result := range dst {
        if result.status == false {
            // 第一个false到来时,必须发布取消命令
            fmt.Printf("%d: I met error\n", result.value)
            done <- struct{}{}
            done <- struct{}{}
            done <- struct{}{}
            done <- struct{}{}
            done <- struct{}{}
            break
        } else {
            fmt.Printf("%d: I have success\n", result.value)
        }
    }
}

func main() {
    eg4()

    time.Sleep(time.Second*5)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

分析一下结果,令人欣慰的是所有的goroutine都结束了,但是有两点缺陷,第一,写了五行done <- struct{}{}是不是很垃圾?第二,在代码中实际受done通道指示结束运行的goroutine只有三条,是不是资源浪费?

其实,最致命的问题是采用缓存通道并不能真正的结束所有该退出的goroutine,想一想,如果在thirtyAPI中继续调用其他API怎么办?我们并不能在预知有多少个goroutine在运行!!!

1.2.3 借助closed channel特性

在1.2.2中,我们知道我们无法预知实际有多少goroutine该执行结束,因而无法确定done channel的长度。

问题似乎不可解,我们不妨换个思路,既然写这条路走不通,那么可否不写呢?

A receive operation on a closed channel can always proceed immediately, yielding the element type's zero value.

当需要下发取消命令时,下发端只需要关闭done channel即可,这样所有需要退出的goroutine都能从done channel读取零值,也就都退出啦!

type Result struct {
    status bool
    value int
}

func thirtyAPI(done chan struct{}, num int, dst chan Result){
    fmt.Printf("我正在调用第三方API:%d\n", num)
    tc := make(chan int, 1)
    i := 0
    for {
        // 业务逻辑代码
        select {
        case <-done:
            fmt.Printf("%d: 我要结束了,Bye ThirtyAPI\n", num)
            dst <- Result{status:false, value:num}
            return
        case tc <- i:
            if num == 3 {
                time.Sleep(time.Second)
                dst <- Result{status: false, value:num}
                return
            }
            if num == 4 {
                dst <- Result{status: true, value: num}
                return
            }
            i = <-tc
            i++
        }
    }
}

func eg4() {
    dst := make(chan Result, 5)
    done := make(chan struct{}, 5)
    defer close(done)
    for i:=0; i<5; i++{
        go thirtyAPI(done, i, dst)
    }

    for result := range dst {
        if result.status == false {
            // 第一个false到来时,必须发布取消命令
            fmt.Printf("%d: I met error\n", result.value)
            break
        } else {
            fmt.Printf("%d: I have success\n", result.value)
        }
    }
}

func main() {
    eg4()

    time.Sleep(time.Second*5)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

其实,Context也正是基于closed channel这个特性实现的。

2 解读Context

2.1 Context接口

type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}
  • Done():该方法返回一个channel,该channel扮演取消信号角色,当该channel被关闭时,所有应该退出的goroutine均可从Done()读值,故而结束执行。
  • Err():打印错误信息,解释为什么context被取消。
  • Deadline(): 返回该context的截止时间,依赖函数可以根据该时间节点为IO操作设定超时时间。
  • Value(key): 该方法根据key返回context对应的属性值,这些值在goroutine之间共享。

2.1.1 基于Context改写1.2.3代码

type Result struct {
    status bool
    value int
}

func thirtyAPI(ctx context.Context, num int, dst chan Result){
    fmt.Printf("我正在调用第三方API:%d\n", num)
    tc := make(chan int, 1)
    i := 0
    for {
        // 业务逻辑代码
        select {
        case <-ctx.Done():
            fmt.Printf("%d: 我要结束了,Error信息: %s\n", num, ctx.Err())
            dst <- Result{status:false, value:num}
            return
        case tc <- i:
            if num == 3 {
                time.Sleep(time.Second)
                dst <- Result{status: false, value:num}
                return
            }
            if num == 4 {
                dst <- Result{status: true, value: num}
                return
            }
            i = <-tc
            i++
        }
    }
}

func eg4() {
    dst := make(chan Result, 5)

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

    for i:=0; i<5; i++{
        go thirtyAPI(ctx, i, dst)
    }

    for result := range dst {
        if result.status == false {
            // 第一个false到来时,必须发布取消命令
            fmt.Printf("%d: I met error\n", result.value)
            break
        } else {
            fmt.Printf("%d: I have success\n", result.value)
        }
    }
}

func main() {
    eg4()

    time.Sleep(time.Second*5)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

2.1.2 Deadline Demo

func gofunc(ctx context.Context) {
    d, _ := ctx.Deadline()

    for {
        select {
        case <-time.After(1 * time.Second):
            fmt.Printf("Deadline:%v, Now:%v\n",d, time.Now())
        case <-ctx.Done():
            fmt.Println(ctx.Err())
            return
        }
    }
}

func main() {
    d := time.Now().Add(5 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), d)
    fmt.Printf("Deadline:%v\n", d)
    defer cancel()
    go gofunc(ctx)

    time.Sleep(time.Second*10)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

2.1.3 Value Demo

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, "Go")

    f(ctx, k)
    f(ctx, favContextKey("color"))

}

2.2 context的函数与Context接口关系

2.2.1 Background vs TODO

3 答疑与最佳实践

3.1 答疑

3.1.1 Context衍生树

The context package provides functions to derive new Context values from existing ones. These values form a tree: when a Context is canceled, all Contexts derived from it are also canceled.

WithCancel and WithTimeout return derived Context values that can be canceled sooner than the parent Context.

对子context的cancel操作,只会影响该子context及其子孙,并不影响其父辈及兄弟context。

package main

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

func child(ctx context.Context, p, c int) {
    fmt.Printf("Child Goroutine:%d-%d\n", p, c)
    select {
    case <-ctx.Done():
        fmt.Printf("Child %d-%d: exited reason: %s\n", p, c, ctx.Err())
    }
}

func parent(ctx context.Context, p int) {
    fmt.Printf("Parent Goroutine:%d\n", p)
    cctx, cancel := context.WithCancel(ctx)
    defer cancel()
    for i:=0; i<3; i++ {
        go child(cctx, p, i)
    }

    if p==3 {
        return
    }

    select {
    case <- ctx.Done():
        fmt.Printf("Parent %d: exited reason: %s\n", p, ctx.Err())
        return
    }
}

func main() {

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for i:=0; i<5; i++ {
        go parent(ctx, i)
    }

    time.Sleep(time.Second*3)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

3.1.2 上下层Goroutine

A Context does not have a Cancel method for the same reason the Done channel is receive-only: the function receiving a cancelation signal is usually not the one that sends the signal. In particular, when a parent operation starts goroutines for sub-operations, those sub-operations should not be able to cancel the parent. Instead, the WithCancel function (described below) provides a way to cancel a new Context value.

Context自身是没有cancel方法的,主要原因是Done channel是只读通道。一般而言,接收取消信号的方法不应该是下发取消信号的。故而,父Goroutine不应该被其创建的子Goroutine取消。

但是,如果在子Goroutine中调用cancel函数,是不是也能取消父Goroutine呢?

package main

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

func SubGor(ctx context.Context, p, c int, cancel context.CancelFunc) {
    fmt.Printf("Child Goroutine:%d-%d\n", p, c)
    if p==2 && c==2 {
        cancel()
    }

    select {
    case <-ctx.Done():
        fmt.Printf("Child %d-%d: exited reason: %s\n", p, c, ctx.Err())
    }
}

func Gor(ctx context.Context, p int,cancel context.CancelFunc) {
    fmt.Printf("Goroutine:%d\n", p)
    for i:=0; i<3; i++ {
        go SubGor(ctx, p, i, cancel)
    }


    select {
    case <- ctx.Done():
        fmt.Printf("Parent %d: exited reason: %s\n", p, ctx.Err())
        return
    }
}


func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for i:=0; i<3; i++ {
        go Gor(ctx, i, cancel)
    }

    time.Sleep(time.Second*3)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

由示例代码可知,如果在子Goroutine调用cancel函数时,一样可以关闭父类Goroutine。但是,不建议这么做,因为它不符合逻辑,cancel应该交给具有cancel权限的人去做,千万不要越俎代庖。

Question:有没有想过context cancel的执行逻辑是什么样子的?

3.1.3 如果goroutine func中不做ctx.Done处理,是不是不会被取消呢?

package main

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

func dealDone(ctx context.Context, i int){
    fmt.Printf("%d: deal done chan\n", i)
    select{
    case <-ctx.Done():
        fmt.Printf("%d: exited, reason: %s\n", i, ctx.Err())
        return
    }
}

func notDealDone(ctx context.Context, i int) {
    fmt.Printf("%d: not deal done chan\n",i)
    for{
        i++
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    for i:=0; i<5; i++ {
        if i==4 {
            go notDealDone(ctx, i)
        } else {
            go dealDone(ctx, i)
        }
    }
    time.Sleep(time.Second*3)
    fmt.Println("Execute Cancel Func")
    cancel()

    time.Sleep(time.Second*3)
    fmt.Printf("Current Active Goroutine Num: %d\n",runtime.NumGoroutine())
}

3.2 最佳实践

Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:

  • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}
  • Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
  • Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
  • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

4 参考链接

相关文章
|
3月前
|
消息中间件 缓存 NoSQL
Redis各类数据结构详细介绍及其在Go语言Gin框架下实践应用
这只是利用Go语言和Gin框架与Redis交互最基础部分展示;根据具体业务需求可能需要更复杂查询、事务处理或订阅发布功能实现更多高级特性应用场景。
309 86
|
5月前
|
分布式计算 算法 安全
Go语言泛型-泛型约束与实践
Go语言中的泛型约束用于限制类型参数的范围,提升类型安全性。通过接口定义约束,可实现对数值类型、排序与比较等操作的支持。开发者既可使用标准库提供的预定义约束,如constraints.Ordered和constraints.Comparable,也可自定义约束以满足特定需求。泛型广泛应用于通用数据结构(如栈、队列)、算法实现(如排序、查找)及构建高效可复用的工具库,使代码更简洁灵活。
|
6月前
|
设计模式 人工智能 Go
go 依赖注入实践
依赖注入(DI)是一种软件设计模式,旨在降低代码耦合度,提高代码可测试性和可复用性。其核心思想是将依赖项从外部传入使用对象,而非由其内部创建。通过 DI,模块间关系更清晰,便于维护和扩展。常见实现包括方法注入和接口注入,适用于如 Go 等支持函数式编程和接口抽象的语言。
148 8
|
6月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:路由、中间件、参数校验
Gin框架以其极简风格、强大路由管理、灵活中间件机制及参数绑定校验系统著称。本文详解其核心功能:1) 路由管理,支持分组与路径参数;2) 中间件机制,实现全局与局部控制;3) 参数绑定,涵盖多种来源;4) 结构体绑定与字段校验,确保数据合法性;5) 自定义校验器扩展功能;6) 统一错误处理提升用户体验。Gin以清晰模块化、流程可控及自动化校验等优势,成为开发者的优选工具。
|
6月前
|
开发框架 安全 前端开发
Go Web开发框架实践:模板渲染与静态资源服务
Gin 是一个功能强大的 Go Web 框架,不仅适用于构建 API 服务,还支持 HTML 模板渲染和静态资源托管。它可以帮助开发者快速搭建中小型网站,并提供灵活的模板语法、自定义函数、静态文件映射等功能,同时兼容 Go 的 html/template 引擎,具备高效且安全的页面渲染能力。
|
6月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
7月前
|
设计模式 缓存 算法
Go如何进行高质量编程与性能调优实践
本文介绍了Go语言高质量编程与性能调优的实践方法。高质量编程包括良好的编码习惯(如清晰注释、命名规范)、代码风格与设计(如MVC模式)、简洁明了的代码原则,以及单元测试与代码重构的重要性。性能调优方面,涵盖算法优化、数据结构选择、I/O优化、内存管理、并行与并发处理优化及代码层面的改进。通过这些方法,可有效提升代码质量和系统性能。
167 13
|
5月前
|
Linux Go 开发者
Go语言泛型-泛型约束与实践
《Go语言实战指南》介绍了如何使用Go进行交叉编译,即在一个操作系统上编译出适用于不同系统和架构的二进制文件。通过设置GOOS和GOARCH环境变量,开发者可轻松构建跨平台程序,无需在每个平台上单独编译。Go从1.5版本起原生支持此功能,极大提升了多平台部署效率。
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####

热门文章

最新文章