Go context 学习

上下文 context.Context Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中我们很少见到类似的概念。

一、简介

context.Context 是 Go 语言在 1.7 版本中引入标准库的接口1,该接口定义了四个需要实现的方法,其中包括:

  1. Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
  2. Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done
    方法会返回同一个 Channel;
  3. Err — 返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;
    如果 context.Context 被取消,会返回 Canceled 错误;
    如果 context.Context 超时,会返回 DeadlineExceeded 错误;
  4. Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

二、操作实例

我们以WaitGroup 和context对比使用

WaitGroup

WaitGroup 是一种控制并发的方式,它的这种方式是控制多个 goroutine 同时完成。

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        time.Sleep(2*time.Second)
        fmt.Println("1号完成")
        wg.Done()
    }()
    go func() {
        time.Sleep(2*time.Second)
        fmt.Println("2号完成")
        wg.Done()
    }()
    wg.Wait()
    fmt.Println("好了,大家都干完了,放工")
}

一个很简单的例子,一定要例子中的 2 个 goroutine 同时做完,才算是完成,先做好的就要等着其他未完成的,所有的 goroutine 要都全部完成才可以。

这是一种控制并发的方式,这种尤其适用于,好多个 goroutine 协同做一件事情的时候,因为每个 goroutine 做的都是这件事情的一部分,只有全部的 goroutine 都完成,这件事情才算是完成,这是等待的方式。

在实际的业务种,我们可能会有这么一种场景:需要我们主动的通知某一个 goroutine 结束。比如我们开启一个后台 goroutine 一直做事情,比如监控,现在不需要了,就需要通知这个监控 goroutine 结束,不然它会一直跑,就泄漏了。

chan 通知

我们都知道一个 goroutine 启动后,我们是无法控制他的,大部分情况是等待它自己结束,那么如果这个 goroutine 是一个不会自己结束的后台 goroutine 呢?比如监控等,会一直运行的。

这种情况化,一直傻瓜式的办法是全局变量,其他地方通过修改这个变量完成结束通知,然后后台 goroutine 不停的检查这个变量,如果发现被通知关闭了,就自我结束。

这种方式也可以,但是首先我们要保证这个变量在多线程下的安全,基于此,有一种更好的方式:chan + select 。

func main() {
    //创建一个双向管道
    stop := make(chan bool)
    go func() {
       // while true,死循环代表这个协程会一直运行
        for {
            select {
            //从stop管道中读取数据,如果有数据且为true,代表有停止的通知停止操作
            case <-stop:
                fmt.Println("监控退出,停止了...")
                return
            default:
                //如果管道中数据是false代表没有通知停止操作
                fmt.Println("goroutine监控中...")
                time.Sleep(2 * time.Second)
            }
        }
    }()
    //主线程睡眠 10 秒
    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    //往双向管道中写入 true,通知停止操作
    stop<- true
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

我们定义一个 stop 的 chan,通知他结束后台 goroutine。实现也非常简单,在后台 goroutine 中,使用 select 判断 stop 是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行 default 里的监控逻辑,继续监控,只到收到 stop 的通知。

这种 chan+select 的方式,是比较优雅的结束一个 goroutine 的方式,不过这种方式也有局限性,如果有很多 goroutine 都需要控制结束怎么办呢?如果这些 goroutine 又衍生了其他更多的 goroutine 怎么办呢?如果一层层的无穷尽的 goroutine 呢?这就非常复杂了,即使我们定义很多 chan 也很难解决这个问题,因为 goroutine 的关系链就导致了这种场景非常复杂。

Context

上面说的这种场景是存在的,比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其他的 goroutine。所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制他们的目的,这就是 Go 语言为我们提供的 Context,称之为上下文非常贴切,它就是 goroutine 的上下文。

func main() {
    //context.Background() 返回一个空的Context,这个空的Context一般用于整个Context树的根节点。然后我们使用context.WithCancel(parent)函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine。
    ctx, cancel := context.WithCancel(context.Background())
    go func(ctx context.Context) {
        for {
            select {
            //使用select调用<-ctx.Done()判断是否要结束,如果接受到值的话,就可以返回结束goroutine了;如果接收不到,就会继续进行监控。
            case <-ctx.Done():
                fmt.Println("监控退出,停止了...")
                return
            default:
                fmt.Println("goroutine监控中...")
                time.Sleep(2 * time.Second)
            }
        }
    }(ctx)
 //睡眠10秒钟
    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    //那么是如何发送结束指令的呢?这就是示例中的cancel函数啦,它是我们调用context.WithCancel(parent)函数生成子Context的时候返回的,第二个返回值就是这个取消函数,它是CancelFunc类型的。我们调用它就可以发出取消指令,然后我们的监控goroutine就会收到信号,就会返回结束。
    cancel()
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}
Context 控制多个 goroutine
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    //示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

    go watch(ctx,"【监控1】")
    go watch(ctx,"【监控2】")
    go watch(ctx,"【监控3】")

    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    cancel()
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name,"监控退出,停止了...")
            return
        default:
            fmt.Println(name,"goroutine监控中...")
            time.Sleep(2 * time.Second)
        }
    }
}
Context 的继承衍生

有了如上的根 Context,那么是如何衍生更多的子 Context 的呢?这就要靠 context 包为我们提供的 With 系列的函数了。

这四个 With 函数,接收的都有一个 partent 参数,就是父 Context,我们要基于这个父 Context 创建出子 Context 的意思,这种方式可以理解为子 Context 对父 Context 的继承,也可以理解为基于父 Context 的衍生。

通过这些函数,就创建了一颗 Context 树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

//WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。 WithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
//WithTimeout和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
//`WithValue`函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过`Context.Value`方法访问到,后面我们会专门讲。
func WithValue(parent Context, key, val interface{}) Context

//大家可能留意到,前三个函数都返回一个取消函数CancelFunc,这是一个函数类型,它的定义非常简单。
//这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的所有的Context,不管有多少层级。
type CancelFunc func()
WithValue 传递元数据

通过 Context 我们也可以传递一些必须的元数据,这些数据会附加在 Context 上以供使用。

var key string="name"

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    //附加值
    //在前面的例子,我们通过传递参数的方式,把name的值传递给监控函数。在这个例子里,我们实现一样的效果,但是通过的是Context的Value的方式
    //这样我们就生成了一个新的Context,这个新的Context带有这个键值对,在使用的时候,可以通过Value方法读取ctx.Value(key)。
//记住,使用WithValue传值,一般是必须的值,不要什么值都传递。

    valueCtx:=context.WithValue(ctx,key,"【监控1】")
    go watch(valueCtx)
    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    cancel()
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}

func watch(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            //取出值
            fmt.Println(ctx.Value(key),"监控退出,停止了...")
            return
        default:
            //取出值
            fmt.Println(ctx.Value(key),"goroutine监控中...")
            time.Sleep(2 * time.Second)
        }
    }
}
Context 使用注意
  1. 不要把 Context 放在结构体中,要以参数的方式传递
  2. 以 Context 作为参数的函数方法,应该把 Context 作为第一个参数,放在第一位。
  3. 给一个函数方法传递 Context 的时候,不要传递 nil,如果不知道传递什么,就使用 context.TODO
  4. Context 的 Value 相关方法应该传递必须的数据,不要什么数据都使用这个传递
  5. Context 是线程安全的,可以放心的在多个 goroutine 中传递
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值