深入Golang之context的用法详解
context在golang的1.7版本之前,是在包golang.org/x/net/context中的,但是后来发现其在很多地方都是需要用到的,所有在1.7开始被列入了golang的标准库。context包专门用来简化处理单个请求的多个goroutine之间与请求域的数据、取消信号、截止时间等相关操作,那么这篇文章就来看看其用法和实现原理。
源码分析
首先我们来看一下context里面核心的几个数据结构:
context interface
type context interface { deadline() (deadline time.time, ok bool) done() <-chan struct{} err() error value(key interface{}) interface{} }
deadline返回一个time.time,是当前context的应该结束的时间,ok表示是否有deadline。
done方法在context被取消或超时时返回一个close的channel,close的channel可以作为广播通知,告诉给context相关的函数要停止当前工作然后返回。
err方法返回context为什么被取消。
value可以让goroutine共享一些数据,当然获得数据是协程安全的。但使用这些数据的时候要注意同步,比如返回了一个map,而这个map的读写则要加锁。
canceler interface
canceler interface定义了提供cancel函数的context:
type canceler interface { cancel(removefromparent bool, err error) done() <-chan struct{} }
其现成的实现有4个:
- emptyctx:空的context,只实现了context interface;
- cancelctx:继承自context并实现了cancelerinterface
- timerctx:继承自cancelctx,可以用来设置timeout;
- valuectx:可以储存一对键值对;
继承context
context包提供了一些函数,协助用户从现有的 context 对象创建新的 context 对象。这些context对象形成一棵树:当一个 context对象被取消时,继承自它的所有context都会被取消。
background是所有context对象树的根,它不能被取消,它是一个emptyctx的实例:
var ( background = new(emptyctx) ) func background() context { return background }
生成context的主要方法
withcancel
func withcancel(parent context) (ctx context, cancel cancelfunc) { c := newcancelctx(parent) propagatecancel(parent, &c) return &c, func() { c.cancel(true, canceled) } }
返回一个cancelctx示例,并返回一个函数,可以在外层直接调用cancelctx.cancel()来取消context。
withdeadline
func withdeadline(parent context, deadline time.time) (context, cancelfunc) { if cur, ok := parent.deadline(); ok && cur.before(deadline) { return withcancel(parent) } c := &timerctx{ cancelctx: newcancelctx(parent), deadline: deadline, } propagatecancel(parent, c) d := time.until(deadline) if d <= 0 { c.cancel(true, deadlineexceeded) // deadline has already passed return c, func() { c.cancel(true, canceled) } } c.mu.lock() defer c.mu.unlock() if c.err == nil { c.timer = time.afterfunc(d, func() { c.cancel(true, deadlineexceeded) }) } return c, func() { c.cancel(true, canceled) } }
返回一个timerctx示例,设置具体的deadline时间,到达 deadline的时候,后代goroutine退出。
withtimeout
func withtimeout(parent context, timeout time.duration) (context, cancelfunc) { return withdeadline(parent, time.now().add(timeout)) }
和withdeadline一样返回一个timerctx示例,实际上就是withdeadline包了一层,直接传入时间的持续时间,结束后退出。
withvalue
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} }
withvalue对应valuectx ,withvalue是在context中设置一个 map,这个context以及它的后代的goroutine都可以拿到map 里的值。
例子
context的使用最多的地方就是在golang的web开发中,在http包的server中,每一个请求在都有一个对应的goroutine去处理。请求处理函数通常会启动额外的goroutine用来访问后端服务,比如数据库和rpc服务。用来处理一个请求的goroutine通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine都应该迅速退出,然后系统才能释放这些goroutine占用的资源。虽然我们不能从外部杀死某个goroutine,所以我就得让它自己结束,之前我们用channel+select的方式,来解决这个问题,但是有些场景实现起来比较麻烦,例如由一个请求衍生出的各个 goroutine之间需要满足一定的约束关系,以实现一些诸如有效期,中止goroutine树,传递请求全局变量之类的功能。
保存上下文
func middleware(next http.handler) http.handler { return http.handlerfunc(func(w http.responsewriter, req *http.request) { ctx := context.withvalue(req.context(),"key","value") next.servehttp(w, req.withcontext(ctx)) }) } func handler(w http.responsewriter, req *http.request) { value := req.context().value("value").(string) fmt.fprintln(w, "value: ", value) return } func main() { http.handle("/", middleware(http.handlerfunc(handler))) http.listenandserve(":8080", nil) }
我们可以在上下文中保存任何的类型的数据,用于在整个请求的生命周期去传递使用。
超时控制
func longrunningcalculation(timecost int)chan string{ result:=make(chan string) go func (){ time.sleep(time.second*(time.duration(timecost))) result<-"done" }() return result } func jobwithtimeouthandler(w http.responsewriter, r * http.request){ ctx,cancel := context.withtimeout(context.background(), 3*time.second) defer cancel() select{ case <-ctx.done(): log.println(ctx.err()) return case result:=<-longrunningcalculation(5): io.writestring(w,result) } return } func main() { http.handle("/", jobwithtimeouthandler) http.listenandserve(":8080", nil) }
这里用一个timerctx来控制一个函数的执行时间,如果超过了这个时间,就会*中断,这样就可以控制一些时间比较长的操作,例如io,rpc调用等等。
除此之外,还有一个重要的就是cancelctx的实例用法,可以在多个goroutine里面使用,这样可以实现信号的广播功能,具体的例子我这里就不再细说了。
总结
context包通过构建树型关系的context,来达到上一层goroutine能对传递给下一层goroutine的控制。可以传递一些变量来共享,可以控制超时,还可以控制多个goroutine的退出。
据说在google,要求golang程序员把context作为第一个参数传递给入口请求和出口请求链路上的每一个函数。这样一方面保证了多个团队开发的golang项目能够良好地协作,另一方面它是一种简单的超时和取消机制,保证了临界区数据在不同的golang项目中顺利传递。
所以善于使用context,对于golang的开发,特别是web开发,是大有裨益的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: SpringBoot项目打jar包
下一篇: 尼康D7000单反相机怎么调整拍摄声音?