登录
首页 >  Golang >  Go教程

go语言context包功能及操作使用详解

来源:脚本之家

时间:2022-12-31 15:57:28 447浏览 收藏

本篇文章主要是结合我之前面试的各种经历和实战开发中遇到的问题解决经验整理的,希望这篇《go语言context包功能及操作使用详解》对你有很大帮助!欢迎收藏,分享给更多的需要的朋友学习~

Context包到底是干嘛用的?

我们会在用到很多东西的时候都看到context的影子,比如gin框架,比如grpc,这东西到底是做啥的?
大家都在用,没几个知道这是干嘛的,知其然而不知其所以然

原理说白了就是:

  • 当前协程取消了,可以通知所有由它创建的子协程退出
  • 当前协程取消了,不会影响到创建它的父级协程的状态
  • 扩展了额外的功能:超时取消、定时取消、可以和子协程共享数据

context原理

这就是context包的核心原理,链式传递context,基于context构造新的context

什么时候应该使用 Context?

  • 每一个 RPC 调用都应该有超时退出的能力,这是比较合理的 API 设计
  • 不仅仅 是超时,你还需要有能力去结束那些不再需要操作的行为
  • context.Context 是 Go 标准的解决方案
  • 任何函数可能被阻塞,或者需要很长时间来完成的,都应该有个 context.Context

如何创建 Context?

在 RPC 开始的时候,使用 context.Background()

有些人把在 main() 里记录一个 context.Background(),然后把这个放到服务器的某个变量里,然后请求来了后从这个变量里继承 context。这么做是不对的。直接每个请求,源自自己的 context.Background() 即可。

如果你没有 context,却需要调用一个 context 的函数的话,用 context.TODO()

如果某步操作需要自己的超时设置的话,给它一个独立的 sub-context(如前面的例子)

主协程通知有子协程,子协程又有多个子协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package main
import (
    "context"
    "fmt"
    "time"
)
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    //缓冲通道预先放置10个消息
    messages := make(chan int, 10)
    defer close(messages)
    for i := 0; i
<p>运行结果如下</p>
<p style="text-align:center"><img alt="" src="/uploads/20221231/167247451763afef9588f74.png"></p>
<p>可以看到,改成context包还是顺利的通过子协程退出了<br>主要修改了几个地方,再ctx向下传递</p>
<p style="text-align:center"><img alt="" src="/uploads/20221231/167247451763afef95b7308.png"></p>
<p>基于上层context再构建当前层级的context</p>
<p style="text-align:center"><img alt="" src="/uploads/20221231/167247451763afef95e6513.png"></p>
<p>监听context的退出信号,</p>
<p style="text-align:center"><img alt="" src="/uploads/20221231/167247451863afef963971c.png"></p>
<p>这就是context包的核心原理,链式传递context,基于context构造新的context</p>
<h2>context核心接口</h2>
<pre class="brush:groovy;">type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done()
<p> Deadline返回绑定当前context的任务被取消的截止时间;如果没有设定期限,将返回ok == false。</p>
<p>Done 当绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil。</p>
<p>Err 如果Done返回的channel没有关闭,将返回nil;如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因。如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded。</p>
<p>Value 返回context存储的键值对中当前key对应的值,如果没有对应的key,则返回nil。</p>
<h3>emptyCtx结构体</h3>
<p>实现了context接口,emptyCtx没有超时时间,不能取消,也不能存储额外信息,所以emptyCtx用来做根节点,一般用Background和TODO来初始化emptyCtx</p>
<h3>Backgroud</h3>
<p>通常用于主函数,初始化以及测试,作为顶层的context</p>
<blockquote><p>context.Background()</p></blockquote>
<h3>TODO</h3>
<p>不确定使用什么用context的时候才会使用</p>
<h3>valueCtx结构体</h3>
<pre class="brush:groovy;">type valueCtx struct{ Context key, val interface{} }
</pre>
<p>valueCtx利用Context的变量来表示父节点context,所以当前context继承了父context的所有信息<br>valueCtx还可以存储键值。</p>
<h3>WithValue向context添加值</h3>
<p>可以向context添加键值</p>
<pre class="brush:groovy;">func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}
</pre>
<p>添加键值会返回创建一个新的valueCtx子节点</p>
<h3>Value向context取值</h3>
<pre class="brush:groovy;">func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}
</pre>
<p>可以用来获取当前context和所有的父节点存储的key</p>
<p>如果当前的context不存在需要的key,会沿着context链向上寻找key对应的值,直到根节点</p>
<h3>示例</h3>
<pre class="brush:groovy;">package main
import (
    "context"
    "fmt"
    "time"
)
func main() {
    ctx := context.WithValue(context.Background(), "name1", "root1")
 
    //第一层
    go func(parent context.Context) {
        ctx = context.WithValue(parent, "name2", "root2")
        //第二层
        go func(parent context.Context) {
            ctx = context.WithValue(parent, "name3", "root3")
            //第三层
            go func(parent context.Context) {
                //可以获取所有的父类的值
                fmt.Println(ctx.Value("name1"))
                fmt.Println(ctx.Value("name2"))
                fmt.Println(ctx.Value("name3"))
                //不存在
                fmt.Println(ctx.Value("name4"))
            }(ctx)
        }(ctx)
    }(ctx)
    time.Sleep(1 * time.Second)
    fmt.Println("end")
}
</pre>
<p>运行结果</p>
<p style="text-align:center"><img alt="" src="/uploads/20221231/167247451863afef96a80ba.png"></p>
<p>可以看到,子context是可以获取所有父级设置过的key</p>
<h2>WithCancel可取消的context</h2>
<p>用来创建一个可取消的context,返回一个context和一个CancelFunc,调用CancelFunc可以触发cancel操作。</p>
<pre class="brush:groovy;">package main
import (
    "context"
    "fmt"
    "time"
)
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    //第一层
    go func(parent context.Context) {
        ctx, _ := context.WithCancel(parent)
        //第二层
        go func(parent context.Context) {
            ctx, _ := context.WithCancel(parent)
            //第三层
            go func(parent context.Context) {
                waitCancel(ctx, 3)
            }(ctx)
            waitCancel(ctx, 2)
        }(ctx)
        waitCancel(ctx, 1)
    }(ctx)
    // 主线程给的结束时间
    time.Sleep(2 * time.Second)
    cancel() // 调用取消context
    time.Sleep(1 * time.Second)
}
func waitCancel(ctx context.Context, i int) {
    for {
        time.Sleep(time.Second)
        select {
        case
<p>结果:</p>
<p style="text-align:center"><img alt="" src="/uploads/20221231/167247451863afef96d59f0.png"></p>
<h3>cancelCtx结构体</h3>
<pre class="brush:groovy;">type cancelCtx struct {
    Context
    mu sync.Mutex
    done chan struct{}
    children map[canceler]struct{}
    err error
}
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}
</pre>
<h2>WithDeadline-超时取消context</h2>
<p>返回一个基于parent的可取消的context,并且过期时间deadline不晚于所设置时间d</p>
<h2>WithTimeout-超时取消context</h2>
<p>创建一个定时取消context,和WithDeadline差不多,WithTimeout是相对时间</p>
<h3>timerCtx结构体</h3>
<p>timerCtx是基于cancelCtx的context精英,是一种可以定时取消的context,过期时间的deadline不晚于所设置的时间d</p>
<p>示例:</p>
<pre class="brush:groovy;">package main
import (
    "context"
    "fmt"
    "time"
)
func main() {
    // 设置超时时间
    ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
    //第一层
    go func(parent context.Context) {
        ctx, _ := context.WithCancel(parent)
        //第二层
        go func(parent context.Context) {
            ctx, _ := context.WithCancel(parent)
            //第三层
            go func(parent context.Context) {
                waitCancel(ctx, 3)
            }(ctx)
            waitCancel(ctx, 2)
        }(ctx)
        waitCancel(ctx, 1)
    }(ctx)
 
     
<p>运行结果:</p>
<pre class="brush:plain;">1 do
3 do
2 do
1 end
3 end
2 end
</pre>
<p>可以看到,虽然我们没有调用cancel方法,5秒后自动调用了,所有的子goroutine都已经收到停止信号</p>
<h2>总结核心原理</h2>
</pre></pre></pre>
  • Done方法返回一个channel
  • 外部通过调用
  • cancel方法会调用close(channel)
    当调用close方法的时候,所有的channel再次从通道获取内容,会返回零值和false
1
res,ok := <-done:
  • 过期自动取消,使用了time.AfterFunc方法,到时调用cancel方法
1
2
3
c.timer = time.AfterFunc(dur, func() {
 c.cancel(true, DeadlineExceeded)
})

今天关于《go语言context包功能及操作使用详解》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!

声明:本文转载于:脚本之家 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>
评论列表