在golang中goroutine是没有主从关系的,是平等的,也就是说goroutineA启动了goroutineB,即使goroutineA结束了,对goroutineB不会有影响,当然如果主程序结束了,所有的goroutine都会结束;在goalng作为服务运行时,各个goroutine的生命周期则需要人为来控制,否则容易出现goroutine泄露的情况。
Golang 的 context Package 提供了一种简洁又强大方式来管理 goroutine 的生命周期,同时提供了一种 Requst-Scope K-V Store。但是对于新手来说,Context 的概念不算非常的直观,这篇文章来带领大家了解一下 Context 包的基本作用和使用方法。
应用场景:在 Go http 包的 Server 中,每一个请求在都有一个对应的goroutine去处理。请求处理函数通常会启动额外的goroutine用来访问后端服务,比如数据库和 RPC 服务。用来处理一个请求的goroutine通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的 token、请求的截止时间。当一个请求被取消或超时时,所有用来处理该请求的goroutine都应该迅速退出,然后系统才能释放这些goroutine占用的资源。
在 go1.7 及以上版本 context 包被正式列入官方库中,所以我们只需要import "context"就可以了,而在 go1.6 及以下版本,我们要 import “golang.org/x/net/context”
通道ch被close(ch)之后,ch的读端会收到消息
p, ok := <-ch
p的值是ch类型的零值
ok为false
它是最基本的接口
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():返回一个struct{}类型的只读 channel
Err():返回 Context 被取消时的错误
Value(key interface{}):是 Context 自带的 K-V 存储功能
为什么 Done() 方法要返回只读的通道呢?
这是出于安全考虑,防止此通道被写入数据从而导致协程退出,设置成只读的话,那就只有 close 这一个操作可以触发通道行为了。
定义了提供 cancel 函数的 context,当然要求数据结构要同时实现 Context interface
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
emptyCtx是空的Context,只实现了Context interface,只能作为 root context 使用。
type emptyCtx int
cancelCtx 继承了 Context 并实现了canceler interface,从WithCancel()函数产生
type cancelCtx struct {
Context
done chan struct{} // closed by the first cancel call.
mu sync.Mutex
children map[canceler]bool // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
cancelCtx 中的 Context 元素为 Context 类型,也就是实现了 Context 接口的变量。
因此cancelCtx也实现了Context。
为什么 cancelctx 还要重新实现 Done 方法呢?
假如传入一个 emptyCtx 作为基础ctx 得到 cancelctx,此时 cancelctx 自然实现了 Context 和 canceler 的 Done 方法,但是这是继承自 emptyCtx 的实现,是这样的:
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
要知道 nil chan 是无法被使用的,否则 panic ,因此需要重新实现
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
这里有一个细节,虽然 Done() 方法只需要返回可读通道,但是必须要 make 一个双向的通道,因为最终 close 一个只读的通道是会报错的,也就是说 c.done 必须为双向通道。
这也是 Done() 方法存在的意义,别看它只是简单的返回一个对象。
timerCtx继承了cancelCtx,所以也自然实现了Context和canceler这两个interface,由WithDeadline()函数产生
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
valueCtx包含key、val field,可以储存一对键值对,由WithValue()函数产生
type valueCtx struct {
Context
key, val interface{}
}
Context 只定义了 interface,真正使用时需要实例化,官方首先定义了一个 emptyCtx struct 来实现 Context interface,然后提供了Backgroud() 和 TODO() 函数来便利的生成一个 emptyCtx 实例。
type emptyCtx int
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
Backgroud() 生成的 emptyCtx 实例是不能取消的,因为emptyCtx没有实现canceler interface,要正常取消功能的话,还需要对 emptyCtx 实例进行派生。常见的两种派生用法是WithCancel(),WithTimeout,WithDeadline。
emptyCtx 没有提供 cancel的 能力。
cancelCtx 包含了emptyCtx,并提供 cancel 的能力。
timerCtx 包含了 cancelCtx,提供了超时后自动调用 cancel 的能力,当然也可以手动 cancel 。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
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 {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
传递一个截止的时间。
timerCtx 有一个 timer 属性,该属性对应一个定时器,
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
初始化一个定时器,到时自动执行 cancel 操作。
即便如此,出于安全考虑,我们还是应该在主代码手动执行 defer cancel()
。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
传递一个超时时间time.Duration,和 WithDeadline 用途一样。
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}
}
context 包可以用来控制协程的退出,协程间共享数据。
一般是这样使用的
go func() {
for {
select {
case <-ctx.Done():
return
case xxx:
yyy
}
}
}()
示例:
func main() {
C6()
}
func C6() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("ctx done")
return
default:
// do some thing
}
}
}(ctx)
rand.Seed(time.Now().UnixNano())
for {
n := rand.Intn(6)
fmt.Println(n)
if n == 5 {
return
}
time.Sleep(1*time.Second)
}
}
程序结束了也没打印 ctx done ,为什么呢?
那时因为主程序结束了,一切都结束了。修改 main 方法:
func main() {
go C6()
for {
time.Sleep(1*time.Second)
}
}
这样就有了。
或许,你就要问了,就这,非得用context吗,我使用通道也能解决啊,因为关闭一个通道,所有的读端也都会收到消息。
func C7() {
clo := make(chan struct{})
defer close(clo)
for i := 0; i < 3; i++ {
// readonly chan
go func(clo <-chan struct{}) {
for {
select {
case <-clo:
fmt.Println("ctx done")
return
default:
// do some thing
}
}
}(clo)
}
rand.Seed(time.Now().UnixNano())
for {
n := rand.Intn(6)
fmt.Println(n)
if n == 5 {
return
}
time.Sleep(1 * time.Second)
}
}
一样的效果,那么context包的必要性在哪里呢?
一个超时的例子
func C2() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
一个WithValue的例子
func C3() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}
context 就是一个工具,有助于我们更方便的实现以上这些功能,使程序显得优雅简洁,但并不是必须的。