zoukankan      html  css  js  c++  java
  • Golang context.Context介绍

    近日某公众号连推2篇关于context的文章,图文不符的错误多处,也不适合我理解,因此查看官方文档后总结一篇笔记。

    context package - context - pkg.go.dev 

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

    本文不会直接讲述context的设计初衷和由来,也不会直接讲述context相比于其他并发控制方式的优劣。

    本文旨在通过解析context包官方文档和示例来探明context的使用方式,从而反推其使用场景。

    这里先贴一下官网文档的Overview部分的简单直译。这部分内容描述了context包的使用方式、主要结构、使用场景,当写完整篇笔记后再来印证可以更深刻理解。

    一、Overview部分直译,为便于理解有删减,完整内容查看官网链接:

    context包提供了Context类型,这种类型可以承载deadlines、取消信号等可以在API边界和进程之间传递消息的对象。

    向server发出请求时应当创建一个context,server处理呼叫应当接收context。整个调用链中context应当作为参数被处理函数传递。这些context可以是也最好是WithCancel, WithDeadline, WithTimeout 或者 WithValue这些衍生出来的child context。当一个Context被取消时,他的child context也会取消。

    WithCancel, WithDeadline, WithTimeout这几个函数接收一个父Context对象,返回子Context对象和一个CancelFunc。当调用对应的CancelFunc时,对应的子Context对象就会被取消。调用CancelFunc失败则child context就会泄露直到父context被取消或者自身超时。

    使用context的程序应当遵循以下规则,以便允许静态分析工具可以获知context的传播链路:

    1. 不要在struct type中存储context,而应当将其作为函数的参数进行传递,即想要使用context时应该给函数额外加一个ctx的参数。

    func DoSomething(ctx context.Context, arg Arg) error {
    	// ... use ctx ...
    }
    

    2. 永远不要传递nil context,如果你不确定该使用哪种context,那么可以先传个context.TODO替代。

    3. Values不应当用作业务参数的传递(虽然这么做确实可以),而应当用来在APIs、processes之间传递消息。

    4. 多个goroutine函数可以共用Context, context是并发安全的。

    可以通过此地址查看示例:Go Concurrency Patterns: Context - go.dev,获知server是如何使用context传递消息的。

    二、context包提供了四种child context使用示例:

    Context接口并不需要我们自己实现,context包已经提供了2个函数(context.Background()和context.TODO())来返回空Context类型,并提供了4个With开头函数来生成具有特定功能的child Context。

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

     WithCancel返回一个child Context ctx,相比于输入的parent,其重写了Done(),实现的功能是:当CancelFunc被调用或parent的Done被写入时,ctx的Done channel会被写入(struct{}{}的空消息),使用ctx的goroutine就可以通过读取ctx.Done()来获知取消信号了。

    package main
    
    import (
    	"context"
    	"fmt"
    )
    
    func main() {
        // gen是一个函数,用于不断的返回整数数字,因为是返回的是只读的unbuffered channel,因此只有当返回值被消费时才会继续返回下一个
    	gen := func(ctx context.Context) <-chan int {
    		dst := make(chan int)
    		n := 1
    		go func() {
                // 在gen内部,通过for select不断的检查ctx.Done()信息来确定自己是否需要return,未接收到ctx.Done()消息时就返回数字等待被消费
    			for {
    				select {
    				case <-ctx.Done():
    					return // 当从ctx.Done()接收到消息时return函数,防止泄露
    				case dst <- n:
    					n++
    				}
    			}
    		}()
    		return dst
    	}
    	// 在gen外部使用WithCancel创建一个ctx,可以看到其parent是context.Background(),context.Background()返回一个非nil的空Context
    	ctx, cancel := context.WithCancel(context.Background())
    	defer cancel() // 当主函数退出时执行cancel函数,此时ctx.Done() channel就会被写入,从而使gen退出
    
        // 主函数遍历gen()的输出,当n=5时break循环,break之后defer cancel()触发,之后上述case <-ctx.Done():被触发从而退出gen函数
    	for n := range gen(ctx) {
    		fmt.Println(n)
    		if n == 5 {
    			break
    		}
    	}
        // 试想下如果没有context会怎样: 当main函数for n := range gen(ctx) break之后,gen()会卡在n=6上无限阻塞而不会释放,这就是goroutine泄露(当然在本例中并不会,因为main函数执行完之后整个进程就退出了)
    
    •  WithDeadline
    func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
    

    WithDeadline返回一个child Context ctx,相比于输入的parent,其deadline不晚于指定的d时刻。如果parent的deadline比d更早,那么按parent的deadline来算。

    什么情况下ctx的Done channel会有消息:1. 当到达deadline时刻 2.CancelFunc被调用 3.parent的Done channel被写入。

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    const shortDuration = 1 * time.Millisecond  
    
    func main() {
    	d := time.Now().Add(shortDuration) // 定义一个基于当前时间1ms后的时刻:d
    	ctx, cancel := context.WithDeadline(context.Background(), d)  // 将d作为ctx超时的时刻
    	defer cancel() // 当main goroutine结束时调用cancel函数
    
    	select {
    	case <-time.After(1 * time.Second):
    		fmt.Println("overslept")
    	case <-ctx.Done():
    		fmt.Println(ctx.Err())
    	}
    	// 检查time.After(1 * time.Second)和ctx.Done()哪个channel有消息,都没消息就阻塞于此一直检查,发现一个有消息就执行对应逻辑然后执行defer cancel()
        // 主函数直到结束才会调用cancel(),time.After(1 * time.Second)时长高达1s,而deadline时刻只有1ms的长度,所以在1ms后ctx.Done()就会因为deadline到达而被写入,因此这个select会在1ms后就直接接收到ctx.Done()消息,然后执行fmt.Println(ctx.Err()),打印出错误:context deadline exceeded。
        // 在这之后继续执行defer cancel()会继续给ctx.Done() channel发消息,那会遇到send on closed channel的panic吗?不会,Done()的返回是幂等的:Successive calls to Done return the same value.。
        // 既然ctx的Done()会因为deadline到达而被提前写入消息,那还有必要defer cancel()吗?官网的解释是有必要,因为这可以确保ctx及其父context的释放。
    }
    
    • WithTimeout
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    

    WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).

    除了这句无需解释了,WithTimeout就是WithDeadline的一个简易入口,使我们可以直接定义ctx多长时间超时,省了自己time.Now().Add(timeout)的步骤。

    示例也不需要了我想。

    • WithValue
    func WithValue(parent Context, key, val interface{}) Context
    

    WithValue相比于之前的3个With开头的函数有区别,他不会返回CancelFunc函数,可以为Context.Value传值。

    但是官网明确说明WithValue的key不应该是字符串或任何其他内置类型,以免与context包自用的产生冲突。应当传递业务自定义类型。直接示例之:

    package main
    
    import (
    	"context"
    	"fmt"
    )
    
    func main() {
    	type favContextKey string  // 自定义一个string的类型:favContextKey
    
        // 定义函数f,接收ctx和k参数
    	f := func(ctx context.Context, k favContextKey) {
            // 获取ctx中存储的key-value pair,如果匹配到了输入参数k对应的值,则把值存入v中并打印
    		if v := ctx.Value(k); v != nil {
    			fmt.Println("found value:", v)
    			return
    		}
            // 如果在ctx中未匹配到k对应的value,那么打印未找到信息
    		fmt.Println("key not found:", k)
    	}
    
    	k := favContextKey("language")
    	ctx := context.WithValue(context.Background(), k, "Go")
        // WithValue返回的ctx存储了一个key-value pair,其key为k,value为Go
    
    	f(ctx, k)
    	f(ctx, favContextKey("color"))
    }
    

    三、总结:

    一般来说当一个goroutine启动之后我们就很难控制他的运行了,除非预先定义了一个channel,然后在goroutine内部不断的检查channel的消息来决定后继运行逻辑。

    基于此逻辑我们来总结context的使用:

    通过上述3个示例,我们可以看到整个context包其实就是围绕Context.Done()这个channel来做文章的。无论是CancelFunc还是Deadline(),Err(),其目的都是辅助Done(),目的就是当满足某些条件时给Done channel传递消息,在goroutine内部则使用select检查ctx.Done()是否有消息来决定下一步的执行逻辑。

    context包提供了一个更人性化的channel定义方式,免了开发者自定义各种通信channel的烦恼。

    与sync.WaitGroup的区别何在?

    很明显的wg用于等到本组内的goroutine自然终结,而context提供了主动终结goroutine的能力,虽然这种能力是建立在需要goroutine内部检查ctx相关状态的基础上的。

    最后WithValue的使用与其他几个有很大区别,看起来更加的灵活,可以为goroutine传递更丰富的消息,有待挖掘补充。

    想建一个数据库技术和编程技术的交流群,用于磨炼提升技术能力,目前主要专注于Golang和Python以及TiDB,MySQL数据库,群号:231338927,建群日期:2019.04.26,截止2021.02.01人数:300人 ... 如发现博客错误,可直接留言指正,感谢。
  • 相关阅读:
    判断分流使用
    Mac系统如何显示隐藏文件?
    Brew安装的软件查询安装位置
    JetBrains 产品线破解方法
    Linux查看与挂载新磁盘
    对BRD、MRD、PRD、FSD四类产品文档的理解
    网站收藏
    收藏
    官方文档
    java 的访问权限控制
  • 原文地址:https://www.cnblogs.com/realcp1018/p/15698693.html
Copyright © 2011-2022 走看看