Golang Context 包详解

0. 引言

在 Go 语言编写的服务器程序中,服务器通常要为每个 HTTP 请求创建一个 goroutine 以并发地处理业务。同时,这个 goroutine 也可能会创建更多的 goroutine 来访问数据库或者 RPC 服务。
当这个请求超时或者被终止的时候,需要优雅地退出所有衍生的 goroutine,并释放资源。因此,我们需要一种机制来通知衍生 goroutine 请求已被取消。 比如以下例子,sleepRandom_1 的结束就无法通知到 sleepRandom_2

package main

import (
    "fmt"
    "time"
)

func sleepRandom_1() {
    i := 0
    for {
        time.Sleep(1 * time.Second)
        fmt.Printf("This is sleep Random 1: %d\n", i)

        i++
        if i == 5 {
            fmt.Println("cancel sleep random 1")
            break
        }
    }
}

func sleepRandom_2() {
    i := 0
    for {
        time.Sleep(1 * time.Second)
        fmt.Printf("This is sleep Random 2: %d\n", i)
        i++
    }
}

func main() {

    go sleepRandom_1() // 循环 5 次后退出
    go sleepRandom_2() // 会一直打印 This is sleep Random 2

    for {
        time.Sleep(1 * time.Second)
        fmt.Println("Continue...")
    }
}

1. Context

Context 包提供上下文机制在 goroutine 之间传递 deadline、取消信号(cancellation signals)或者其他请求相关的信息。使用方法是:

  1. 首先,服务器程序为每个接受的请求创建一个 Context 实例(称为根 context,通过 context.Background() 方法创建);
  2. 之后的 goroutine 接受根 context 的一个派生 Context 对象。比如通过调用根 context 的 WithCancel 方法,创建子 context;
  3. goroutine 通过 context.Done() 方法监听取消信号。func Done() <-chan struct{} 是一个通信操作,会阻塞 goroutine,直到收到取消信号接触阻塞。
    (可以借助 select 语句,如果收到取消信号,就退出 goroutine;否则,默认子句是继续执行 goroutine);
  4. 当一个 Context 被取消(比如执行了 cancelFunc()),那么该 context 派生出来的 context 也会被取消。

1.1 Context 类型

// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {

    Done() <-chan struct{}

    Deadline() (deadline time.Time, ok bool)

    Err() error

    Value(key interface{}) interface{}
}

Done() <-chan struct{}

Done 方法返回一个 channel,阻塞当前运行的代码,直到以下条件之一发生时,channel 才会被关闭,进而解除阻塞:

  1. WithCancel 创建的 context,cancelFunc 被调用。该 context 以及派生子 context 的 Done channel 都会收到取消信号;
  2. WithDeadline 创建的 context,deadline 到期。
  3. WithTimeout 创建的 context,timeout 到期

Done 要配合 select 语句使用:

// DoSomething 生产数据并发送给通道 out
// 但如果 DoSomething 返回一个则退出函数,
// 或者 ctx.Done 被关闭时也会退出函数.
func Stream(ctx context.Context, out chan<- Value) error {
    for {
        v, err := DoSomething(ctx)
        if err != nil {
            return err
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case out <- v:
        }
    }
}

Deadline() (deadline time.Time, ok bool)

WithDeadline 方法会给 context 设置 deadline,到期自动发送取消信号。调用 Deadline() 返回 deadline 的值。如果没设置,ok 返回 false。
该方法可用于确定当前时间是否临近 deadline。

Err() error

如果 Done 的 channel 被关闭了, Err 函数会返回一个 error,说明错误原因:

  1. 如果 channel 是因为被取消而关闭,打印 canceled;
  2. 如果 channel 是因为 deadline 到时了,打印 deadline exceeded。

重复调用,返回相同值。

Value(key interface{}) interface{}

返回由 WithValue 关联到 context 的值。

1.2 创建根 Context

有两种方法创建根 Context:

  1. context.Background()
  2. context.TODO()

根 context 不会被 cancel。这两个方法只能用在最外层代码中,比如 main 函数里。一般使用 Background() 方法创建根 context。
TODO() 用于当前不确定使用何种 context,留待以后调整。

1.3 派生 Context

一个 Context 被 cancel,那么它的派生 context 都会收到取消信号(表现为 context.Done() 返回的 channel 收到值)。
有四种方法派生 context :

  1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

  2. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

  3. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

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

WithCancel

最常用的派生 context 方法。该方法接受一个父 context。父 context 可以是一个 background context 或其他 context。
返回的 cancelFunc,如果被调用,会导致 Done channel 关闭。因此,绝不要把 cancelFunc 传给其他方法。

WithDeadline

该方法会创建一个带有 deadline 的 context。当 deadline 到期后,该 context 以及该 context 的可能子 context 会受到 cancel 通知。
另外,如果 deadline 前调用 cancelFunc 则会提前发送取消通知。

WithTimeout

与 WithDeadline 类似。创建一个带有超时机制的 context。

WithValue

WithValue 方法创建一个携带信息的 context,可以是 user 信息、认证 token等。该 context 与其派生的子 context 都会携带这些信息。

WithValue 方法的第二个参数是信息的唯一 key。该 key 类型不应对外暴露,为了避免与其他包可能的 key 类型冲突。所以使用 WithValue 也
应像下面例子的方式间接调用 WithValue。

WithValue 方法的第三个参数即是真正要存到 context 中的值。

使用 WithValue 的例子:

package user

import "context"

// User 类型对象会被保存到 Context 中
type User struct {
    // ...
}

// key 不应该暴露出来。这样避免与包中其他 key 类型冲突
type key int

// userKey 是 user 的 key,不应暴露;
// 通过 user.NewContext 和 user.FromContext 间接使用 key
var userKey key

// NewContext 返回携带 u 作为 value 的 Context
func NewContext(ctx context.Context, u *User) context.Context {
    return context.WithValue(ctx, userKey, u)
}

// FromContext 返回关联到 context 的 User类型的 value 的值
func FromContext(ctx context.Context) (*User, bool) {
    u, ok := ctx.Value(userKey).(*User)
    return u, ok
}

2. 例子

改进引子里的例子。 sleepRandom_1 结束后,会触发 cancelParent() 被调用。所以 sleepRandom_2 中的 ctx.Done() 会被关闭。
sleepRandom_2 执行退出。

package main

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

func sleepRandom_1(stopChan chan struct{}) {
    i := 0
    for {
        time.Sleep(1 * time.Second)
        fmt.Printf("This is sleep Random 1: %d\n", i)

        i++
        if i == 5 {
            fmt.Println("cancel sleep random 1")
            stopChan <- struct{}{}
            break
        }
    }
}

func sleepRandom_2(ctx context.Context) {
    i := 0
    for {
        time.Sleep(1 * time.Second)
        fmt.Printf("This is sleep Random 2: %d\n", i)
        i++

        select {
        case <-ctx.Done():
            fmt.Printf("Why? %s\n", ctx.Err())
            fmt.Println("cancel sleep random 2")
            return
        default:
        }
    }
}

func main() {

    ctxParent, cancelParent := context.WithCancel(context.Background())
    ctxChild, _ := context.WithCancel(ctxParent)

    stopChan := make(chan struct{})

    go sleepRandom_1(stopChan)
    go sleepRandom_2(ctxChild)

    select {
    case <- stopChan:
        fmt.Println("stopChan received")
    }
    cancelParent()

    for {
        time.Sleep(1 * time.Second)
        fmt.Println("Continue...")
    }
}

3. 参考文档

Go Concurrency Patterns: Context

Understanding the context package in golang

Golang Context 包详解的更多相关文章

  1. 【GoLang】golang context channel 详解

    代码示例: package main import ( "fmt" "time" "golang.org/x/net/context" ) ...

  2. golang context用法详解

    背景 在go服务器中,对于每个请求的request都是在单独的goroutine中进行的,处理一个request也可能设计多个goroutine之间的交互, 使用context可以使开发者方便的在这些 ...

  3. Golang官方log包详解

    Golang官方log包详解 以下全是代码, 详解在注释中, 请从头到尾看 // Copyright 2009 The Go Authors. All rights reserved. // Use ...

  4. Spring jar包详解

    Spring jar包详解 org.springframework.aop ——Spring的面向切面编程,提供AOP(面向切面编程)的实现 org.springframework.asm——spri ...

  5. Spring——jar包详解(转)

    Spring——jar包详解 org.springframework.aop ——Spring的面向切面编程,提供AOP(面向切面编程)的实现 org.springframework.asm——spr ...

  6. Spring 3.x jar 包详解 与 依赖关系

    以下的内容我会持续更新(当然是我有新发现的时候); 以下内容是我在网上搜索.整理.修改的而成的内容.由于很多内容都是转载了,无法追溯到源头,因此无法一一对原作者进行道谢. 这几天,我查阅大量的官方的文 ...

  7. Spring 3.x jar 包详解 与 依赖关系(转)

    以下的内容我会持续更新(当然是我有新发现的时候); 以下内容是我在网上搜索.整理.修改的而成的内容.由于很多内容都是转载了,无法追溯到源头,因此无法一一对原作者进行道谢. 这几天,我查阅大量的官方的文 ...

  8. 常见 jar包详解

        常见 jar包详解 jar包 用途 axis.jar SOAP引擎包 commons-discovery-0.2.jar 用来发现.查找和实现可插入式接口,提供一些一般类实例化.单件的生命周期 ...

  9. spring原理案例-基本项目搭建 02 spring jar包详解 spring jar包的用途

    Spring4 Jar包详解 SpringJava Spring AOP: Spring的面向切面编程,提供AOP(面向切面编程)的实现 Spring Aspects: Spring提供的对Aspec ...

随机推荐

  1. mongodb 3 常用命令操作

    操作命令详见,这个归类很好,有些教程乱麻麻的 http://www.tuicool.com/articles/j2ueau db.createUser({user:"zhihuiroot&q ...

  2. Java_Java中动态加载jar文件和class文件

    转自:http://blog.csdn.net/mousebaby808/article/details/31788325 概述 诸如tomcat这样的服务器,在启动的时候会加载应用程序中lib目录下 ...

  3. IOS第九天(1:QQ聊天界面frame模型)

    ///  控制层 #import "HMViewController.h" #import "HMMessageModel.h" #import "H ...

  4. mysql中count(),group by使用

    count()统计表中或数组中记录 count(*)返回检索行的数目,且不论其值中是否包含NULL count(column_name)返回的是对列中column_name不为NULL的行的统计 例如 ...

  5. 每日一九度之 题目1042:Coincidence

    时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:3007 解决:1638 题目描述: Find a longest common subsequence of two strings. 输入 ...

  6. 06_在web项目中集成Spring

    在web项目中集成Spring 一.使用Servlet进行集成测试 1.直接在Servlet 加载Spring 配置文件 ApplicationContext applicationContext = ...

  7. 实现android上解析Json格式数据功能

    实现android上解析Json格式数据功能,该源码转载于安卓教程网的,http://android.662p.com ,个人感觉还不错的,大家可以看看一下吧. package com.practic ...

  8. iOS里面消除使用代理调用方法时间警告问题

    iOS里面有三种调用函数的方式: 直接调用方法   [对象名 方法]; performselector:    [对象名 perform方法]; NSInvocation     调用 在使用代理调用 ...

  9. 内网穿透+VS2015自带IIS express实现本地调试(微信等需要将开发环境暴漏到外网的情况使用)

    今天一个兼职结束了,又要开始寻找新的兼职公司了 ,为了贴补家用啊,为了给儿子更好的生活加油! 抒情完毕进入正题,本篇文章要解决的问题是其实在开发微信支付,微信公众号等回调地址必须是外网可访问的80端口 ...

  10. 8.21.2 深入finally语句快

    关于finally语句块 1.finally语句块可以直接和try语句块联用. try....finally... 2.try...catch....finally 也可以. 3.在finally语句 ...