context 包是用来干啥的
这包是用来干啥的可能直接看官方的文档最合适:
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
从这里看出 context包主要用来在跨api,goroutine传递消息(deadline, cancellation signals, and other scoped values)。在golang 一般业务逻辑和底层是分开的,他们属于两个goroutine,context可以个更优雅的在他们之间传播消息。
可以简单看下Context 包含的内容。
type Context interface {
// 获取一个有deadline contxt 的过期时间,如果该context 没有deadline
// ok 为false
Deadline() (deadline time.Time, ok bool)
// 一个只读 channel,等待 context结束(被cancel,达到deadline)
Done() <-chan struct{}
// 如果Done 没有比close,返回nil,否则返回被close的原因
// canceled for context was canceled
// DeadlineExceeded for context deadline passed
Err() error
// 携带的数据
Value(key interface{}) interface{}
}
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
返回父 context的一个拷贝,以下两种情况下Done channel 会被关闭
- 返回的cancel方法被主动调用
- 父context的Done channel被关闭
由于Context 是跨goroutine 之间传递的,这里只要父进程或者父进程的父进程被cancel了,新的context 就会收到 Done的消息。下面来看一下官方的一个例子:
package main
import (
"context"
"fmt"
)
func main() {
// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done(): // 等待 context 被cancel
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
// 跳出for 循环结束,之后会执行上面的defer cancel,
// 然后在gen 就会收到context Done channel被关闭的通知
break
}
}
}
WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
返回父 Context 的一个拷贝,在以下几种情况下Done channel会被关闭
- deadline 过期
- cancel 函数被调用
- 父 Context 的Done channel 被关闭
这里需要注意的是如果 父 Context 本身也是一个 WithDeadline的情况
- 如果 父 Context 的 deadline 比d要早直接返回父 Context
- 如果 d已经过期,设置子context的error 为 DeadlineExceeded
WithTimeout
WithDeadline的变体, 等于 WithDeadline(parent, time.Now().Add(timeout))
WithValue
在context 中附加(key,value) pair
常见使用方法
WithCancel
在处理http请求的时候,如果这个请求在业务逻辑处理完成之前,底层socket被关闭的时候取消上面的业务逻辑(以此再去做业务逻辑已经没啥意义了)。
// 请求处理函数
func handler(r *http.Request, w *http.ResponseWriter) {
ctx, cancel := newCtx2(context.TODO(), w)
defer cancel()
doSomeThing(ctx, r) // 业务逻辑处理函数
}
// 构建一个新的 context
func newCtx2(ctx context.Context, w *http.ResponseWriter) (context.Context, context.CancelFunc) {
var cancel context.CancelFunc
if cn, ok := w.(http.CloseNotifier); ok {
// 底层socket 被关闭
ctx, cancel = context.WithCancel(ctx)
notifyChan := cn.CloseNotify()
go func() {
select {
case <-notifyChan:
cancel() // cancel context
case <-ctx.Done():
}
}()
} else {
cancel = nilCancel
}
return ctx, cancel
}
func nilCancel() {}
WithTimeout
处理 http client 请求超时处理, 这里请求 www.google.com,如果超时返回 ctx.Err() 返回 deadline exceed,否则打印success 信息
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.TODO(), time.Millisecond*2)
defer cancel()
done := make(chan struct{})
go doRequest(ctx, done) // 业务请求
select {
case <-ctx.Done():// 超时
d, _ := ctx.Deadline()
fmt.Println("err = ", ctx.Err(), d)
case <-done: // 正常处理结束
fmt.Println("successful handle the request")
}
}
func doRequest(ctx context.Context, done chan struct{}) {
req, _ := http.NewRequest("GET", "http://www.google.com", nil)
req.WithContext(ctx)
http.DefaultClient.Do(req)
close(done)
}
这里只罗列了这两种简单的用法,跨goroutine, 快api的只要有cancel, 处理timeout逻辑的都可以使用这种逻辑去处理。
内部实现原理
context 包的实现主要依赖 timerCtx 和 cancelCtx 这两个结构体, 这里也主要分析他俩的代码。
Cancel Context
下面是内部 concelCtx的定义
type cancelCtx struct {
Context // 父 Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
// 存放该 context 派生出来的子context
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
下面就以构建一个 WithCancel 的context 来看下具体的实现代码
// 构建一个cancel ctx
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil { // 确保父 contxt 不为空
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent) // 产生一个上面定义的cancelCtx
propagateCancel(parent, &c) // 将上面派生出来的 contxt 放到父 context 对应的 children 字段中
return &c, func() { c.cancel(true, Canceled) }
}
// 给派生出来的 context 安排一个合适的位置, 以便父 context 被取消的时候能够通知到children
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
// 查找 parent 对应的 cancelCtx,如果父 context 是一个可 cancel 的context,将 child 放到 parent.children 中(父context cancel的时候需要通知到子 context)
if p, ok := parentCancelCtx(parent); ok { //
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{} // 将 child 添加到
}
p.mu.Unlock()
} else { // 父 context 不是一个cancel context
atomic.AddInt32(&goroutines, +1)
// 在这里可以看到 如果父 context 不是一个可 cancel 的context的时候,是单独启一个goroutine 等等 context 结束的。这也是为啥一定要调用 defer cancel()的目的,不然就会发生goroutine 泄漏。
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
下面来看下一个concel 的处理过程
// CancelFunc 的原始定义
type CancelFunc func()
// 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 { // 第一次调用 cancel 之后 c.err 就会设置为 err,
这里如果 c.err已经被设置了就说明,该context 已经被cancel了
c.mu.Unlock()
return // already canceled
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done) // 通知单独起的 goroutine, 该context 被cancel
}
// 通知所有的派生 context,这里递归的调用cancel 每个一派生的context
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()
// 将自己从 父 context 中移除
if removeFromParent {
removeChild(c.Context, c)
}
}
至此 cancelContext 的代码也就分析完了,整体过程是这样的 创建:
- 第一次需要派生一个cancel context的时候会起一个goouritne去等到 Done 的channel
- 如果已经是一个 cancel context 了则将将 派生出的 context 放到 parent的children中
cancel:
a. 如果已经被cancel 直接返回
b. close(c.done) 通知上面创建中a 起的goroutine
c. cancel 所有的children, 以及 children cancel 自己的children
Deadline Context
把上面的 cancel context 坑透之后再去看 deadline context 就简单多了, 下面一个deadline contxt 底层 timerCtx 的定义:
type timerCtx struct {
cancelCtx // timer context的好多方法都会委托给cancelCtx
timer *time.Timer // 对应 timer
deadline time.Time // deadline 时间点
}
下面来看下一个timerContext 的构造过程:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
// parent 是一个deadlinie context 并且,parent.deadline > d
// 直接返回 父 context(因为父 context 被取消的时候,对应的所有子孙都会被取消,所以 child.deadline > parent.deadline 就没意义了)
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
// 构造 timerCtx
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 和cancel context 一样,给新派生出来的context 安排一个合适的位置,以便父 context 被cancel 的时候通知到个子 context
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 { // 已经过期
// 注意一下由于上面已经c 放到 parent.children 或者,单独其一个goroutine了, 这里需要回退掉
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil { // 没有被cancel 或者deadline excedd
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded) // dur 之后自动cancel
})
}
return c, func() { c.cancel(true, Canceled) }
}
下面也看下 deadline context 被取消的过程
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err) // 委托给 cancelCtx
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
// 取消timer
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
避坑指南
给你一把瑞士军刀,如果用不好可能很容易伤到自己,context 也是,在业务中有些东西也要避免滥用。
- 如果不是需要跨多个api之间传递的参数就不要带到context 中, 能直接使用参数形式传递的就尽量使用参数形式传递。
- api里面不要保存传进来的context,只应该让他在api之间流转
- 不要传递一个nil 给context
- 记得 **defer cancel() **去释放资源
- context 按照约定放到函数的第一个参数
func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}
- 一定要理解context中 chain的概念,一个context可以不断的派生出contxt, 只要一个conext 被cancel,他派生出来的子,子的子 等等都会被关闭。
参考: