为什么使用Context
go的扛把子
要论go最津津乐道的功能莫过于go强大而简洁的并发能力。
func main() {
go func() {
fmt.Println("Hello world")
}()
}
通过简单的go func( ){ },go可以快速生成新的协程并运行。
想象一个没有Context的世界
有并发的地方就有江湖,每个编程语言都有各自的并发编程
方式,也有不同的并发控制方法,比如java通过join( )
来做主子线程同步。Java里面还有ThreadLocal
的概念,但是在go里面也没有。
go里面常用于协程
间通信和管理的有channel和sync包。比如channel可以通知协程做特定操作(退出, 阻塞)等,sync可以加锁和同步。
Context基础用法
最为重要的就是3个基础能力,取消
,超时
,附加值
。
新建一个Context
ctx := context.TODO()
ctx := context.BackGround()
这两个方法返回的内容都是一样的,都是返回一个空的context,这个context一般用来做父context。
WithCancel
// 函数声明
func WithCancel(parentCtx context.Context) (ctx context.Context, cancel context.CancelFunc){
// 用法返回一个子Context 和 主动取消函数
ctx, cancel := context.WithCancel(parentCtx)
}
这个函数相当重要,会根据传入的context
生成一个子context
和一个取消函数
。当父context有相关取消操作,或者直接调用cancel函数的话,子context就会被取消。
举个日常业务中的常用的例子:
// 一般操作比较耗时或者涉及远程调用等,都是在输入参数里带上一个ctx
func Do(parentCtx context.Context) {
ctx, cancel := context.WithCancel(parentCtx)
// 实现某些逻辑业务
// 当遇到某种条件,比如程序出错,就取消掉子Context
if err != nil {
cancel()
}
}
WithTimeout
// 函数声明
func WithTimeout(parentCtx context.Context, timeout time.Duration) (ctx context.Context, cancel context.CancelFunc) {
// 用法:返回一个子COntext 和 主动取消函数
ctx, cancel := context.WithTimeout(parentCtx, time.Second)
}
这个函数在日常工作中使用得非常多,简单来说就是给Context附加一个超时控制,当超时ctx.Done( )
返回的channel就能读取到值,协程可以通过这个方式来判断执行时间是否满足要求。
WithDeadLine
// 函数声明
func WithCancel(parentCtx context.Context, d time.Time) (ctx context.Context, cancel context.CancelFunc) {
// 用法返回一个子Context(会在指定的时间自动取消) 和 主动取消函数
ctx, cancel := context.WithDeadline(parentCtx, time.Now().Add(5*time.Second))
}
这个函数感觉用得比较少,和WithTimeout相比的话就是使用的是截至时间。
示例:
func TestParentContext(t *testing.T) {
ctx := context.Background()
dlCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Minute))
childCtx := context.WithValue(dlCtx, "name", 123)
cancel()
err := childCtx.Err()
fmt.Println(err)
}
运行结果:
WithValue
// 函数声明
func WithValue(parent context.Context, key, val interface{}) {
// 用法:传入父Context和(key,value),相当于存一个kv
childCtx := context.WithValue(parent, "name", 123)
v := childCtx.Value("name")
fmt.Println(v)
}
这个函数常用来保存一些链路追踪信息,比如API服务里会由来保存一些源IP,请求参数等。
这个方法很常用!
Context源码及接口
Context是一个接口
虽然我们平时写代码时直接context.Context
拿来就用,但实际上context.Context是一个接口,源码里是有多种不同的实现的,借此实现不同的功能。
type Context interface {
// 返回这个ctx预期的结束时间
Deadline() (deadline time.Time, ok bool)
// 返回一个channel,当执行结束或者取消时被close,我们平时可以用这个来判断ctx绑定的协程是否该退出。实现里用的懒汉模式,所以一开始可能会返回nil
Done() <-chan struct{}
// 如果未完成,返回nil。已完成源码里目前就两种错误,已被取消或者已超时
Err() error
// 返回ctx绑定的key对应的value值
Value(key interface{}) interface{}
}
Context们是一颗树
context整体是一个树形结构
,不同的ctx间可能是兄弟节点或者是父子节点的关系。
同时由于Context接口有多种不同的实现,所以树的节点可能也是多种不同的ctx实现。总的来说我觉得Context的特点是:
1、树形结构
,每次调用WithCancel
,WithValue
,WithTimeout
,WithDeadline
实际是为当前节点在追加子节点。
2、继承性
,某个节点被取消,其对应的子树也会全部被取消。
3、多样性
,节点存在不同的实现,故每个节点会附带不同的功能。
Context的孩子们
在源码里实现只有4种实现,要弄懂context的源码其实把4种对应的实现学习一下就行,他们分别是:
emptyCtx
: 一个空的ctx,一般用于做根节点
cancelCtx
:核心,用来处理相关的取消操作。
timeCtx
:用来处理超时相关操作。
valueCtx
:附加值的实现方法。
cancelCtx
结构
done
:用于判断是否完成
cancel
:用于取消节点
err
:取消时的错误,超时或主动取消
cancelCtx的取消实现
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
整个过程可以总结为:
1、前置判断,看是否为异常情况
2、关闭c.done,这样外部调用cancelCtx.Done()就会有返回结果。
3、递归调用子节点的cancel方法
4、视情况从父节点中移除子节点
总结
cancelCtx作用其实就两个:
1、绑定父子节点,同步取消信号,父节点取消子节点也会跟着取消
。
2、提供主动取消函数
。
Context安全传递数据
context包我们就用来做两件事:
1、安全传递数据
2、控制链路
安全传递数据,是指在请求中执行上下文线程安全的传递数据,依赖于WithValue方法
Context父子关系
特点:context的实例之间存在父子关系
1、当父亲取消或者超时,所有派生的子Context都会被取消或者超时。
2、当找key的时候,子Context先看自己有没有,没有则取祖先里找。
func TestParentValueContext(t *testing.T) {
ctx := context.Background()
childCtx := context.WithValue(ctx, "key1", 123)
ccCtx := context.WithValue(childCtx, "key2", 124)
v := childCtx.Value("key2")
fmt.Println(v)
v = ccCtx.Value("key1")
fmt.Println(v)
}
输出结果:
控制是自上而下的,查找是自下而上的。
Context控制超时
最经典的用法是利用context控制超时。
控制超时,相当于我们监听两个channel
,一个业务正常结束的channel,还有一个Done() 返回的。
func TestContextBusinessTimeOut(t *testing.T) {
ctx := context.Background()
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
end := make(chan struct{}, 1)
go func() {
MyBusiness()
end <- struct{}{}
}()
ch := timeoutCtx.Done()
select {
// 超时分支
case <-ch:
fmt.Println("time out")
// 正常业务分支
case <-end:
fmt.Println("business end")
}
}
func MyBusiness() {
time.Sleep(500 * time.Millisecond)
fmt.Println("hello world")
}
运行结果:
Context包使用注意事项
1、一般只用作方法参数,而且是作为第一个参数。
2、所有公共方法,除非是util,helper之类的方法,否则都加上Context参数
3、不要用作结构体字段,除非你的结构体本身传达的也是一个上下文的概念。
Context包–面试要点
1、context.Context使用场景:上下文传递和超时控制。
2、context.Context原理:
父亲如何控制儿子:通过儿子主动加入到父亲的children里面,父亲只需遍历就可以。
3、valueCtx和timeCtx的原理:
总结
Context的主要功能就是用于控制协程退出和附加链路信息。核心实现的结构体有4个,最复杂的是cancelCtx,最常用的是cancelCtx和valueCtx。整天呈树状结构,父子节点间同步取消信号。