go context用法简介

目录:
1、什么是context
2、context用法

  • 2.1、创建子context
  • 2.2、创建context 树
  • 2.3、如何使用
  • 2.4、使用注意事项

1、什么是context:

Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine(协程) 的上下文,包含 goroutine 的运行状态、环境、现场等信息。context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。

context.Context 类型的值可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。最重要的是它是并发安全的。
与它协作的 API 都可以由外部控制执行“取消”操作,例如:取消一个 HTTP 请求的执行。

在Go 里,我们不能直接杀死协程,协程的关闭一般会用 channel+select 方式来控制。但是在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel+select 就会比较麻烦,这时就可以通过 context 来实现。

单的说context 用来解决 goroutine 之间退出通知、 元数据传递的功能。


2、context用法:

假设你开启了一个函数,你需要将一些常用的值传递给下游函数,但是不能通过函数参数传递,怎么办?
假设你开启了一个协程 A,协程 A 衍生出很多子协程,这些子协程又衍生出子协程,如果协程 A 所完成的任务“成果”不再需要,那我们怎么通知衍生出的子协程及时退出并释放占用的系统资源呢?
假设一个任务需要在 2s 内完成,如果超时,如何优雅地退出返回呢?
假设一个任务需要在中午 12 点完成,如果到点没有完成,又该如何优雅地退出呢?
golang提供了context包来处理上面的问题:

理解 context 包,核心是需要理解 Context 接口:

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

这个接口有四个方法:

  • Done():返回只读的 channel,在 goroutine 中,如果该 channel 可读,则意味着父 context 发起了取消操作或者是时间到期,理解这一点非常重要。
  • Err():返回错误,表示 channel 被关闭的原因,被取消还是超时。
  • Deadline():获取设置的截止时间,两个返回的参数第一个是截止时间,表示到了这个点,context 会自动发起取消操作;第二个表示是否设置了截止时间。
  • Value():方法获取 context 上绑定的值,是一个键值对,这个值一般是线程安全的。

2.1、创建子context:

我们不需要自己实现 Context 接口,源码包已经为我们提供了两个实现接口的方法,分别是 Background() 和 TODO();

var (
 background = new(emptyCtx)
 todo       = new(emptyCtx)
)

func Background() Context {
 return background
}

func TODO() Context {
 return todo
}

context.Background() 返回空的 context,通常用在 main 函数里,作为根 context 衍生出子 context。
context.TODO() 也是返回空 context。主要用在还不清楚使用什么类型的 context 的时候,便于后期重构,先用它占个位。
它们两本质上是 emptyCtx 类型,不能被取消,没有值,也没有超时时间。

有了上面两个根 context,就可以衍生出子 context,源码包为我们提供了一系列 withXXX 函数用于生成子 context,分别是:

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

这四个函数的第一个参数都是父 context,可以理解为基于父 context 生成子 context,即衍生子 context。

  • WithCancel() 基于父 context,返回子 context 和取消函数;
  • WithDeadline() 基于父 context,返回带截止时间(截止时间可以基于当前时间延后多少时间)的子 context 和取消函数;
  • WithTimeout() 基于父 context,返回带超时时间的子 context 和取消函数;
  • WithValue() 基于父 context,返回绑定键值对的子 context,没有取消函数;

前三个函数都会返回取消函数,需要注意的是只有创建该 context 的协程才能调用取消函数,且不推荐将取消函数作为参数传递。
我们可以调用取消函数取消一个 context,以及这个 context 下面所有的子 context。
通过这些函数,就能生成一棵 context 树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

2.2、创建context 树:

我们一起来看下基于两个根 context 可以创建的 context 树是什么样。
1)、两层树:

rootCtx := context.Background()
childCtx := context.WithValue(rootCtx, "request_Id", "seekload")

上面的代码基于根 context 创建了两层 context 树,rootCtx 衍生出 childCtx,并携带键值对 {"request_Id" : "seekload"}。

2)、三层树:

rootCtx := context.Background()
childCtx := context.WithValue(rootCtx, "request_Id", "seekload")
childOfChildCtx, cancelFunc := context.WithCancel(childCtx)

基于两层树,childCtx 衍生出 childOfChildCtx,含有键值对并且具有取消功能。

3)、多层树:

rootCtx := context.Background()
childCtx1 := context.WithValue(rootCtx, "request_Id", "seekload")
childCtx2, cancelFunc := context.WithCancel(childCtx1)
childCtx3 := context.WithValue(rootCtx, "user_Id", "user_100")

上面的代码:
rootCtx 是根 context;
rootCtx 衍生出 childCtx1,并携带键值对 {"request_Id" : "seekload"};
childCtx1 衍生出 childCtx2,可以取消 context;
rootCtx 衍生出 childCtx3,携带键值对 {"user_Id" : "user_100"};
层级关系就像下面这样:


context.png

2.3、如何使用:

1)、context.WithCancel()用法:
context.WithCancel() 用于取消信号,直接来看例子:

func main() {
    ctx := context.Background() //创建根context
    cancelCtx, cancelFunc := context.WithCancel(ctx) //基于根创建第二层context对象,传入协程中
    go task(cancelCtx)
    time.Sleep(time.Second * 3)
    cancelFunc()                        // 可以在父级对创建的子context发起取消 context(即结束协程)
    time.Sleep(time.Second * 1)         // 延时等待协程退出
    fmt.Println("number of goroutine: ",runtime.NumGoroutine()) // 协程数量
}

func task(ctx context.Context) {
    i := 1
    for {
        select {
        case <-ctx.Done(): // 接收取消信号
            fmt.Println("Gracefully exit")
            fmt.Println(ctx.Err()) // 取消原因
            return
        default:
            fmt.Println(i)
            time.Sleep(time.Second * 1)
            i++
        }
    }
}

输出:

1
2
3
Gracefully exit
context canceled
number of goroutine:  1

当调用 cancelFunc(),Done() 返回的 channel 变成可读,Err() 返回取消原因 “context canceled”,task() 函数执行 return 优雅地退出。

2)、context.WithValue()用法:
通过 context.WithValue() 可以在 goroutine 之间传递一些数据。

func main() {
    helloWorldHandler := http.HandlerFunc(HelloWorld)
    http.Handle("/hello", inejctRequestId(helloWorldHandler))
    http.ListenAndServe(":8080", nil)
}

func HelloWorld(w http.ResponseWriter, r *http.Request) {
    requestId := ""
    if m := r.Context().Value("requestId"); m != nil {
        if value, ok := m.(string); ok {
            requestId = value
        }
    }
    w.Header().Add("requestId", requestId)
    w.Write([]byte("Hello, world"))
}

func inejctRequestId(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestId := uuid.New().String()
        ctx := context.WithValue(r.Context(), "requestId", requestId)
        req := r.WithContext(ctx)
        next.ServeHTTP(w, req)
    })
}

上面的代码,inejctRequestId() 是请求中间函数,通过 context.WithValue() 注入了键值对;HelloWorld() 是请求处理函数,从 context 获取到刚才绑定的 k-v。
go run 上面示例,然后执行:

$curl -v localhost:8080/hello
会输出:

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Requestid: e0b0544d-7993-4ff5-a2de-b29eacd3645a
< Date: Tue, 11 Jul 2023 08:03:26 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
Hello, world

从输出可以看到,返回里有 Requestid 信息。

3)、context.WithTimeout()用法:
context.WithTimeout() 可以设置一个超时时间,过期之后 channel done 会自动关闭,context 会被取消;超时之前可以调用取消函数手动取消 context。

func main() {
    ctx := context.Background()
    cancelCtx, cancel := context.WithTimeout(ctx, time.Second*3)
    defer cancel()
    go task(cancelCtx)
    time.Sleep(time.Second * 4)
}

func task(ctx context.Context) {
    i := 1
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Gracefully exit")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println(i)
            time.Sleep(time.Second * 1)
            i++
        }
    }
}

输出:

1
2
3
Gracefully exit
context deadline exceeded

上面的代码,context.WithTimeout() 设置了 3s 的超时时间,时间到了之后,context 自动取消,done channel 变成可读,Err() 返回取消原因,执行 return,task() 优雅地退出。

4)、context.WithDeadline()用法:
context.WithDeadline() 设置一个将来的时间点作为截止时间,时间到了之后,channel done 会自动关闭,context 会被取消;还未到截止时间可以调用取消函数手动取消 context。

func main() {
    ctx := context.Background()
    cancelCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*3))
    defer cancel()
    go task(cancelCtx)
    time.Sleep(time.Second * 4)   // 延时,等待 task() 正常退出
}

func task(ctx context.Context) {
    i := 1
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Gracefully exit")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println(i)
            time.Sleep(time.Second * 1)
            i++
        }
    }
}

上面的代码设置的截止时间是 3s 钟之后的时间点,时间到了之后,context 自动取消,done channel 变成可读,Err() 返回取消原因,执行 return,task() 优雅地退出。
输出:

1
2
3
Gracefully exit
context deadline exceeded

2.4、使用注意事项:

  1. 永远不要传递不存在的 context 。相反,如果您不确定使用什么,使用一个 ToDo context。
  2. context.Background 只应用在最高等级,作为所有派生 context 的根。
  3. Context 结构没有取消方法,因为只有派生 context 的函数才应该取消 context。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容