假设goroutine派生出子goroutine,子goroutine又继续派生新的,这种情况下使用WaitGroup比较麻烦,因为子goroutine个数不容易确定,而使用context就可以很容易实现
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
该方法获取设置的截止时间, deadline 为截止时间, 到了这个时间 ctx 会自动发起取消请求. 如果没有设置截止时间, 那么需要手动调用 cancel() 方法来停止. ok==false时表示没有设置截止时间
该方法返回一个只读的 channel 类型是 struct{}, 需要在select-case语句中使用,如”case <-context.Done():”当有信号时,表明parent context 已经发起了取消, goroutine 中通过 Done chan 获取到取消信号后, 应当做清理操作,然后退出协程,释放资源
- 因deadline关闭:“context deadline exceeded”;
- 因主动关闭: “context canceled”。
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
var background = new(emptyCtx)
func Background() Context {
return background
}
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // 维护了所有的衍生节点
err error // set to non-nil by the first cancel call
}
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
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
c.mu.Lock()
c.err = err //设置一个error,说明关闭原因
close(c.done) //将channel关闭,以此通知派生的context
for child := range c.children { //遍历所有children,逐个调用cancel方法
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent { //正常情况下,需要将自己从parent删除
removeChild(c.Context, c)
}
}
- 初始化一个cancelCtx实例
- 将cancelCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
- 返回cancelCtx实例和cancel()方法
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c) //将自身添加到父节点
return &c, func() { c.cancel(true, Canceled) }
}
func propagateCancel(parent Context, child canceler) {
...
if p, ok := parentCancelCtx(parent); ok {
// 当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号;
p.mu.Lock()
if p.err != nil {
// 如果已经被取消,child 会立刻被取消;
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// 如果没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号;
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
// 运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel
// 在 parent.Done() 关闭时调用 child.cancel 取消子上下文;
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
- 如果父节点也支持cancel,也就是说其父节点肯定有children成员,那么把新context添加到children里即可;
- 如果父节点不支持cancel,就继续向上查询,直到找到一个支持cancel的节点,把新context添加到children里;
- 如果所有的父节点均不支持cancel,则启动一个协程等待父节点结束,然后再把当前context结束
//src/context/context.go:timerCtx
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
- deadline: 指定最后期限,比如context将2018.10.20 00:00:00之时自动结束
- timeout: 指定最长存活时间,比如context将在30s后结束
Deadline()方法仅仅是返回timerCtx.deadline而矣。而timerCtx.deadline是WithDeadline()或WithTimeout()方法设置的
- 如果deadline到来之前手动关闭,则关闭原因与cancelCtx显示一致;
- 如果deadline到来时自动关闭,则原因为:”context deadline exceeded”
- 初始化一个timerCtx实例
- 将timerCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
- 启动定时器,定时器到期后会自动cancel本context
- 返回timerCtx实例和cancel()方法
WithTimeout()实际调用了WithDeadline,二者实现原理一致,源码
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
package main
import (
"fmt"
"time"
"context"
)
func HandelRequest(ctx context.Context) {
go WriteRedis(ctx)
go WriteDatabase(ctx)
for {
select {
case <-ctx.Done():
fmt.Println("HandelRequest Done.")
return
default:
fmt.Println("HandelRequest running")
time.Sleep(2 * time.Second)
}
}
}
func WriteRedis(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("WriteRedis Done.")
return
default:
fmt.Println("WriteRedis running")
time.Sleep(2 * time.Second)
}
}
}
func WriteDatabase(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("WriteDatabase Done.")
return
default:
fmt.Println("WriteDatabase running")
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx, _ := context.WithTimeout(context.Background(), 5 * time.Second)
go HandelRequest(ctx)
time.Sleep(10 * time.Second)
}
func Background() Context
context的呈现的形式像二叉树结构,有父子关系,父协程管理子协程。context的使用场景就是主协程管理多个子协程,这边的管理就是简单粗暴的关闭子协程
//生成一个带取消函数cancelFunc的子上下文ctx(通过调用返回的cancelFunc,释放一个信号,此时从ctx.Done()这个channel就可以获取到取消的信号)
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
package main
import (
"context"
"fmt"
"time"
)
func dosomething(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("playing")
return
default:
fmt.Println("I am working!")
time.Sleep(time.Second)
}
}
}
func main() {
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
time.Sleep(5 * time.Second)
cancelFunc()
}()
dosomething(ctx)
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
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}
}
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
...
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
...
}
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
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
//时间过了 cancelCtx
return WithCancel(parent)
}
//实例timeCtx
c := &timeCtx{
//cancel的上下文
cancelCtx: newCancelCtx(parent),
deadline: d,
}
//cancelCtx也调用了这个函数
propagateCancel(parent, c)
//时间差
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded)
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)
}
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
//调用WithDeadline, 当前时间+timeout,时间到了 done管道收到信息,函数自行关闭
return WithDeadline(parent, time.Now().Add(timeout))
}
func (c *timeCtx) cancel(removeFromParent bool, err error) {
//调用cancelCtx的cancel进行关闭
c.cancelCtx.cancel(false, err)
if removeFromParent {
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
defer c.mu.Unlock()
//关闭
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
}
package main
import (
"context"
"fmt"
)
func main() {
//1.获取一个空context
ctx := context.Background()
process(ctx)
//2.向context写数据
ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
process(ctx)
}
func process(ctx context.Context) {
//2.读取context数据
traceId, ok := ctx.Value("traceId").(string)
if ok {
fmt.Printf("process over. trace_id=%s\n", traceId)
} else {
fmt.Printf("process over. no trace_id\n")
}
}
const requestIDKey int = 0
func WithRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(
func(rw http.ResponseWriter, req *http.Request) {
// 从 header 中提取 request-id
reqID := req.Header.Get("X-Request-ID")
// 创建 valueCtx。使用自定义的类型,不容易冲突
ctx := context.WithValue(
req.Context(), requestIDKey, reqID)
// 创建新的请求
req = req.WithContext(ctx)
// 调用 HTTP 处理函数
next.ServeHTTP(rw, req)
}
)
}
// 获取 request-id
func GetRequestID(ctx context.Context) string {
ctx.Value(requestIDKey).(string)
}
func Handle(rw http.ResponseWriter, req *http.Request) {
// 拿到 reqId,后面可以记录日志等等
reqID := GetRequestID(req.Context())
...
}
func main() {
handler := WithRequestID(http.HandlerFunc(Handle))
http.ListenAndServe("/", handler)
}
func Perform() {
for {
//计算获取结果
calculatePos()
//发送
sendResult()
//延迟
time.Sleep(time.Second)
}
}
func Perform(ctx context.Context) {
for {
calculatePos()
sendResult()
select {
case <-ctx.Done():
// 通过<-ctx.Done()监听取消状态,被取消时直接返回
return
case <-time.After(time.Second):
// block 1 秒钟
}
}
}
//1.调用WithTimeout()创建一个ctx,返回ctx 与取消函数cancel
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
//2.调用业务接口
go Perform(ctx)
//3.当判断需要取消时,直接再此处取消即可
cancel()
WithTimeOut 函数返回的 context 和 cancelFun 是分开的。context 本身并没有取消函数,这样做的原因是取消函数只能由外层函数调用,防止子节点 context 调用取消函数,从而严格控制信息的流向:由父节点 context 流向子节点 context
func gen() <-chan int {
ch := make(chan int)
go func() {
var n int
for {
ch <- n
n++
time.Sleep(time.Second)
}
}()
return ch
}
//如果只需要它产生的前 5 个数,那么就会发生 goroutine 泄漏
func main() {
for n := range gen() {
fmt.Println(n)
if n == 5 {
break
}
}
// ……
}
func gen(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
var n int
for {
select {
case <-ctx.Done():
return
case ch <- n:
n++
time.Sleep(time.Second)
}
}
}()
return ch
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
cancel()
break
}
}
// ……
}
或者通过select监听context的ctx.Done()当取消时此处会拿到消息,可以进行指定处理
func main() {
// 创建一个超时时间为100毫秒的上下文
ctx := context.Background()
ctx, _ = context.WithTimeout(ctx, 100*time.Millisecond)
// 创建一个访问Google主页的请求
req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
// 将超时上下文关联到创建的请求上
req = req.WithContext(ctx)
// 创建一个HTTP客户端并执行请求
client := &http.Client{}
res, err := client.Do(req)
// 如果请求失败了,记录到STDOUT
if err != nil {
fmt.Println("Request failed:", err)
return
}
// 请求成功后打印状态码
fmt.Println("Response received, status code:", res.StatusCode)
}
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
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}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
- 不要把Context放在结构体中,要以参数的方式显示传递
- 以Context作为参数的函数方法,应该把Context作为第一个参数。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
- Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
- Context是线程安全的,可以放心的在多个goroutine中传递
- Deadline获取设置的截止时间方法,
- Done返回一个只读的 channel 类型是 struct{}方法,当有信号时,表明parent context 已经发起了取消,通过 Done chan 获取到取消信号后, 应当做清理操作,然后退出协程,释放资源,
- Err返回 ctx 为什么被取消方法,
- Value获取 ctx 上绑定的值
- emptyCtx可以获取一个空context,
- cancelCtx: 被cancle时会把其中的所有child都cancle掉
- timerCtx: 在cancelCtx基础上增加了deadline用于标示自动cancel的最终时间
- WithCancel() 生成一个带取消函数的子上下文
- WithDeadline() 与 WithTimeOut() 接收过期时间,生成一个带取消函数的子上下文
- 超时取消: 使用WithTimeout()获取一个超时自动取消的context, 后续在执行中通过select监听Done.执行取消业务
- 基于context的values缓存数据
- 参考博客
- 参考博客
- 参考博客
- Go 语言问题集(Go Questions)