草庐IT

go 与 context.Context

ankerjian 2023-03-28 原文

本文作者:anker

前言

context.Context 非常重要!
context.Context 非常重要!
context.Context 非常重要!

源码

go1.17.6的 context.Context 源码,建议所有 golang developer 都读一读,在旧版本没有那么多解释文字,在最新的版本补充了很详细的说明,甚至样例,足以说明 context.Context 在 golang 中的重要性。

这里大概讲解一下提供的方法及其作用,后面会详细举例

method 作用
Deadline() (deadline time.Time, ok bool) 返回 Context 所控制的 deadline,ok表示是否有设置 deadline;一般用于 goroutine 控制超时时间
Done() <-chan struct{} 返回一个 chan,当 Context 被 cancel 或者达到 dealine的时候,此 chan 会被 close
Err() error Done 方法返回的 chan 被 close 以后有用,返回是什么原因被 close
Value(key interface{}) interface{} 通过传入 key 获取 value,用于 Context 链式传递值

如果暂时不想阅读源码,可以先跳过这小节,后面都会讲到。当然自行看看会更加清晰!

Context 存在的意义?

先给一个定论:Context 用于协程生命周期的管理,包括值传递控制传递

首先我们先来看看 context 包的常用方法

  • func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
  • func WithValue(parent Context, key, val interface{}) Context
  • func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
  • func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

不难看出,上面几个方法,通过父 Context 和一些参数派生出新的 Context 和 CancelFunc(非必须)。这里 CancelFunc 的作用是通知新的 Context 以及往后通过他派生出来的 Context 进行结束。

cancel 行为

那么通过派生行为,可以把 Context 管理的控制行为传递下去。

那么 Context 的作用,其实也很明显了:

作用 样例
管理生命周期参数 通过 Context 传递 RequestId 打日志从 Context 读取,把一个请求内的日志串起来
管理控制行为 监听进程关闭信号量,通过 Context 通知协程尽快结束

举例

参数传递

这里以 http 请求通过 Context 传递 requestId 到日志组件作为例子,下面是样例代码。

样例代码

package main

import (
    "context"
    "fmt"
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "sync/atomic"
)

const requestIdCtxKey = "requestIdCtxKey"

func main() {
    counter := uint64(0)
    r := gin.Default()
    r.Use(func(c *gin.Context) {
        // gin.Context 也实现了 context.Context interface
        requestId := atomic.AddUint64(&counter, 1)
        // 给每个请求设置 requestId
        c.Set(requestIdCtxKey, requestId)
    })

    r.GET("/", func(c *gin.Context) {
        // 读取相应内容,以 ctx.Context 读取
        getResponseMessage := func(ctx context.Context, username string) (message string) {
            message = fmt.Sprintf("hello %s!", username)
            logFunc(ctx, "response:"+message)
            return
        }

        // 直接把 gin.Context 传入
        message := getResponseMessage(c, c.DefaultQuery("user", "guest"))

        logFunc(c, "url:"+c.Request.URL.RequestURI())
        c.String(http.StatusOK, message)
    })
    r.Run(":12222")
}

func logFunc(ctx context.Context, message string) {
    requestId := ctx.Value(requestIdCtxKey)
    id := requestId.(uint64)
    log.Printf("requestId:%d message:%s\n", id, message)
}

执行命令:

curl http://127.0.0.1:12222/?user=anker

http输出:
hello anker!

标准输出:
2022/04/03 21:18:43 requestId:1 message:response:hello anker!
2022/04/03 21:18:43 requestId:1 message:url:/?user=anker

控制传递

这里以控制协程结束为例,下面是样例代码

package main

import (
    "context"
    "log"
    "sync"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    // 五秒后通知 ctx 结束
    time.AfterFunc(5*time.Second, func() {
        cancel()
    })

    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        ticker := time.NewTicker(1 * time.Second)
        defer ticker.Stop()
        for ok := true; ok; {
            select {
            case <-ctx.Done():
                // 监听到结束,设置循环结束条件
                ok = false
                if ctx.Err() != nil {
                    // 输出退出原因
                    log.Println("context done,error:", ctx.Err().Error())
                }
            case <-ticker.C:
            }

            if !ok {
                continue
            }

            // 每1秒输出一次
            log.Println("goroutine 1 ticker:", time.Now().Unix())
        }
    }()

    wg.Add(1)
    go func() {
        defer wg.Done()
        // 监听ctx结束
        <-ctx.Done()
        log.Println("goroutine 2 end")

    }()

    wg.Wait()
    log.Println("exit ok")
}

标准输出:

2022/04/03 21:32:50 goroutine 1 ticker: 1648992770
2022/04/03 21:32:51 goroutine 1 ticker: 1648992771
2022/04/03 21:32:52 goroutine 1 ticker: 1648992772
2022/04/03 21:32:53 goroutine 1 ticker: 1648992773
2022/04/03 21:32:54 goroutine 2 end
2022/04/03 21:32:54 goroutine 1 ticker: 1648992774
2022/04/03 21:32:54 context done,error: context canceled
2022/04/03 21:32:54 exit ok

延伸开来,可以通过在函数调用链中传递 Context ,实现在上层函数一些机制(超时、可用性检测)通知下层方法尽快结束。

例如 Mysql 查询,Redis 操作,调用这些组件的库一般都会提供 Context 作为函数第一个值,其实就是用作控制行为传递。

而且强烈建议 Context 作为函数的参数时,放第一个参数位置。

最后

通过一些样例,现在应该对 Context 有更深的了解了。不妨再滑动上去看看源码中关于 Context 的注释,会有更深刻的认识。

有关go 与 context.Context的更多相关文章

  1. ruby-on-rails - Textmate 'Go to symbol' 相当于 Vim - 2

    在Railcasts上,我注意到一个非常有趣的功能“转到符号”窗口。它像Command-T一样工作,但显示当前文件中可用的类和方法。如何在vim中获取它? 最佳答案 尝试:helptags有各种程序和脚本可以生成标记文件。此外,标记文件格式非常简单,因此很容易将sed(1)或类似的脚本组合在一起,无论您使用何种语言,它们都可以生成标记文件。轻松获取标记文件(除了下载生成器之外)的关键在于格式化样式而不是实际解析语法。 关于ruby-on-rails-Textmate'Gotosymbol

  2. ruby - 使用 cucumber/aruba/timecop : How to inject the context 测试命令行应用程序 - 2

    以下场景几乎概括了我的问题:Scenario:problemswithsubprocessesGiventhedateis01/01/201210:31WhenIrun`ruby-e"putsTime.now"`Thentheoutputshouldcontain"10:31"它归结为当我运行ruby​​-e"putsTime.now"时启动一个子进程,从而使我所有的Timecop.freezestub无效,因为他们只在主要过程中工作。我需要以某种方式将当前上下文“注入(inject)”到运行的命令中,但我似乎无法想出任何东西。我在这里尝试不可能的事情吗?步骤:require'time

  3. ruby-on-rails - 用于 Ruby 的 vim 中的全局 "Go to definition"? - 2

    自97年以来我一直在使用vi/vim进行各种快速编辑和管理任务,但最近才考虑使用它来替换Netbeans作为我选择的ruby​​编辑器。我发现一件事在Netbeans和Eclipse中非常有用的是Ctrl+Click“转到定义”功能,您可以在其中按住Ctrl键并单击一个类或方法,然后它将带您了解定义。现在,我玩过丰富的ctags和rails.vim,而且很接近,但没有雪茄。这就是我想要的:默认情况下在Netbeans和Eclipse中,您可以在本地rails中按住ctrl并单击本地方法或类项目,但你也可以ctrl+click定义在gems或用Ruby编写的系统库。以Netbeans为例

  4. ruby-on-rails - Rails 上的 ruby : How to have multiple submit buttons going to different methods (maybe with with_action? ) - 2

    这个问题在这里已经有了答案:HowdoIcreatemultiplesubmitbuttonsforthesameforminRails?(7个答案)关闭9年前。所以..'save'%>'library'%>然后在我的Controller中:with_actiondo|a|a.savedoenda.librarydoendend问题是只有一个操作被调用...两个submit_tags调用相同的操作...知道为什么吗?或者我如何获得两个按钮以将表单提交给两种不同的方法?

  5. DiFi: A Go-as-You-Pay Wi-Fi Access System 精读笔记(三) - 2

    IV.SYSTEMIMPLEMENTATIONWeadoptmodulardesignfollowingtheintegrationofblockchain.Itbringsmoreflexibilitybyseparatingtheimplementationofdifferentfunctionalities,sowecouldleveragetheadvantagesoftheblockchain-basedsmartcontractwhilereducingoverhead.Figure3illustrateshowdifferentmodulesareinvolvedintheint

  6. ruby - 如何避免 RSpec 中的 "Useless use of == in void context"? - 2

    在RSpec中,如果我有警告并且有x.should==42another_line_of_code然后我得到一个关于的警告warning:uselessuseof==invoidcontext还有什么我可以做的吗关闭警告将其更改为bitbucket=(x.should==42) 最佳答案 使用:x.shouldeq(42)或者:x.shouldbe==42或者移动x.should==42使其成为itblock中的最后一行。对于那些思考但是为什么?的人我完全是Ruby的菜鸟,但这是我的理解:警告来自Ruby,因为像x.should==

  7. go-templates - 如何根据表达式有条件地在 Go 模板中设置变量,如果不使用 if 语句包装可能会导致错误 - 2

    问题我该如何做这样的事情:{{$use_ssl:=(ne$.Env.CERT_NAME"")}}其中$.Env.CERT_NAME可能为零/未定义。如果它是零,它给出这个错误:at:errorcallingne:invalidtypeforcomparison注意:我无法控制传递给Go模板的对象,因此必须完全在模板本身内解决这个问题。我尝试过的我试图通过首先检查它是否为非空来变通:{{$use_ssl:=(($.Env.CERT_NAME)&&(ne$.Env.CERT_NAME""))}}但它给出了这个错误:unexpected"&"inoperand所以我切换到这个,这在语法上是允

  8. Go time与string的相爱相杀 - 2

    time包与string包可以说是在Go语言的开发中常用的两个包实际开发过程中(例如web开发)经常会遇到time类型与string类型的交互,计算比较等场景首先来了解GO语言里非常浪漫的一个点,即2006-01-0215:04:05,GO语言诞生的时间,通常用来做时间的格式化time转stringt:=time.Now()//当前时间timeLayoutStr:="2006-01-0215:04:05"t.Format(timeLayoutStr)//返回值为string,可以用一个值来接收它上述例子中,将time类型t转换为string类型,并格式化为年-月-日时-分-秒,这里的格式化是可

  9. ruby - Pry Error : Cannot find local context. 您是否使用了 `binding.pry`? - 2

    为什么我会收到此pry动错误?[36]pry(main)>s="pry"Error:Cannotfindlocalcontext.Didyouuse`binding.pry`?在此截屏视频中运行良好http://pryrepl.org/ 最佳答案 似乎s、c和n是pry-navgem上的保留命令,发现here,这可以帮助您逐步完成绑定(bind)。Pry.commands.alias_command'c','continue'Pry.commands.alias_command's','step'Pry.commands.alias

  10. javascript - xul : creating a right click context menu item for only hyperlinks - 2

    我有一个关于构建firefox插件的问题,基本上我的目标是做以下事情,1)在我的插件中,我只想为链接[anchortags]显示右键单击上下文菜单项,并为页面的其余部分隐藏菜单项2)如何将动态列表添加到我的菜单中,即根据用户的选择动态添加菜单列表项的数量。谁能给我指出正确的方向谢谢!! 最佳答案 为contextmenu事件绑定(bind)一个事件监听器,判断被点击的元素是否为链接,例如:window.addEventListener("contextmenu",function(e){varmenu=document.getEle

随机推荐