Context(上下文)是Go语言中一个非常重要的概念,它主要用于在多个goroutine之间传递请求相关的数据、取消信号以及超时信息。Context在Go 1.7版本中被正式引入标准库,现已成为处理并发控制和请求作用域数据的标准方式。
Context的核心思想是提供一种统一的方式来管理goroutine的生命周期,特别是在处理网络请求、RPC调用或任何需要跨API边界传递截止时间和取消信号的场景中。
在Go的并发编程中,我们经常会遇到以下几个问题:
Context就是为了解决这些问题而设计的。它提供了一种标准的、可组合的方式来处理截止时间、取消信号和请求作用域的值。
Context的核心是context.Context
接口,定义如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
创建Context通常从"background"或"todo"这两个根Context开始:
// 通常用作main函数、初始化和测试中,作为顶层Context
ctx := context.Background()
// 当不确定使用哪种Context时使用,通常用于重构过程中
ctx := context.TODO()
从已有的Context可以派生出新的Context,形成Context树。当父Context被取消时,所有派生出的子Context也会被取消。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
创建一个可取消的Context,返回的cancel函数用于取消该Context。
示例:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保在函数退出时取消Context
go func() {
select {
case <-ctx.Done():
fmt.Println("Context canceled:", ctx.Err())
}
}()
time.Sleep(time.Second)
cancel() // 手动取消Context
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
创建一个具有截止时间的Context,当截止时间到达时自动取消。
示例:
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("Operation completed")
case <-ctx.Done():
fmt.Println("Context deadline exceeded:", ctx.Err())
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
创建一个具有超时时间的Context,是WithDeadline的便捷包装。
示例:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("Operation completed")
case <-ctx.Done():
fmt.Println("Context timeout:", ctx.Err())
}
func WithValue(parent Context, key, val interface{}) Context
创建一个携带键值对的Context,用于在请求范围内传递数据。
示例:
type contextKey string
const (
requestIDKey contextKey = "requestID"
userIDKey contextKey = "userID"
)
ctx := context.WithValue(context.Background(), requestIDKey, "12345")
ctx = context.WithValue(ctx, userIDKey, "[email protected]")
fmt.Println("Request ID:", ctx.Value(requestIDKey))
fmt.Println("User ID:", ctx.Value(userIDKey))
Context的实现基于一种树形结构,每个Context都保持对其父Context的引用。当创建一个派生的Context时,实际上是创建了一个新的Context节点,该节点指向其父节点。
当调用cancel函数取消一个Context时,所有从它派生的Context也会被取消。这是通过从当前Context节点向上遍历到根节点,然后向下通知所有子节点来实现的。
[Background/TODO Context]
|
v
[WithCancel/WithDeadline/WithTimeout Context]
|
v
[WithValue Context] --> [存储键值对]
|
v
[使用Context的goroutines]
当取消信号触发时:
[取消信号触发]
|
v
[关闭Done channel]
|
v
[通知所有子Context]
|
v
[所有关联的goroutines收到取消信号]
在HTTP服务器中,Context可用于处理请求超时和传递请求作用域的值。
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 设置超时
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
// 传递请求ID
ctx = context.WithValue(ctx, requestIDKey, generateRequestID())
result, err := doSomeWork(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Result: %v", result)
}
func doSomeWork(ctx context.Context) (string, error) {
// 检查Context是否已取消
if err := ctx.Err(); err != nil {
return "", err
}
// 模拟耗时操作
select {
case <-time.After(3 * time.Second):
return "Work done", nil
case <-ctx.Done():
return "", fmt.Errorf("work canceled: %v", ctx.Err())
}
}
func queryDatabase(ctx context.Context, query string) ([]string, error) {
// 模拟数据库查询
results := make(chan []string)
errs := make(chan error)
go func() {
// 模拟耗时查询
time.Sleep(3 * time.Second)
// 检查Context是否已取消
select {
case <-ctx.Done():
return
default:
}
// 返回结果
results <- []string{"result1", "result2"}
}()
select {
case r := <-results:
return r, nil
case err := <-errs:
return nil, err
case <-ctx.Done():
return nil, fmt.Errorf("query canceled: %v", ctx.Err())
}
}
func processTasks(ctx context.Context, tasks []string) ([]string, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var wg sync.WaitGroup
results := make(chan string, len(tasks))
errChan := make(chan error, 1)
for _, task := range tasks {
wg.Add(1)
go func(t string) {
defer wg.Done()
// 处理任务
result, err := processTask(ctx, t)
if err != nil {
select {
case errChan <- err:
cancel() // 取消其他任务
default:
}
return
}
select {
case results <- result:
case <-ctx.Done():
}
}(task)
}
// 等待所有任务完成
go func() {
wg.Wait()
close(results)
}()
var processed []string
for {
select {
case err := <-errChan:
return nil, err
case result, ok := <-results:
if !ok {
return processed, nil
}
processed = append(processed, result)
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
忘记调用cancel函数可能导致内存泄漏,因为Context会保持对其父Context的引用。解决方案是总是使用defer cancel()。
使用字符串作为key可能导致包之间的冲突。解决方案是使用自定义类型:
type contextKey string
const (
requestIDKey contextKey = "requestID"
)
ctx := context.WithValue(context.Background(), requestIDKey, "123")
当多个goroutine共享同一个Context时,一个goroutine的失败可能导致其他goroutine被过早取消。解决方案是为每个goroutine创建独立的Context。
虽然大多数情况下使用标准库的Context就足够了,但有时可能需要自定义实现。例如,实现一个记录取消原因的Context:
type cancelCtx struct {
context.Context
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
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 WithCancelReason(parent context.Context) (context.Context, context.CancelFunc) {
ctx := &cancelCtx{Context: parent}
return ctx, func() { ctx.cancel(context.Canceled) }
}
func (c *cancelCtx) cancel(err error) {
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children {
child.cancel(err)
}
c.children = nil
c.mu.Unlock()
}
func mergeContexts(ctx1, ctx2 context.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
select {
case <-ctx1.Done():
cancel()
case <-ctx2.Done():
cancel()
case <-ctx.Done():
}
}()
return ctx, cancel
}
Context是Go语言并发编程中的重要工具,它提供了一种标准化的方式来管理goroutine的生命周期、传递请求作用域的值以及处理超时和取消。正确使用Context可以编写出更健壮、更易维护的并发代码。
关键要点:
通过合理使用Context,可以有效地解决Go并发编程中的许多常见问题,编写出更优雅、更可靠的代码。