Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Golang Context.WithCancel

Golang中Context.WithCancel 的实战指南

作者:言之。

本文介绍了Go语言中context包的核心用法,重点讲解了context.Background()和context.WithCancel()的使用场景,具有一定的参考价值,感兴趣的可以了解一下

1. 它到底做了什么

关键点:取消是向下传播的。取消父 ctx,会取消它的所有子孙;取消子 ctx,不会影响父亲或兄弟。

2. 何时应当用WithCancel(context.Background())

如果是捕获系统信号(Ctrl+C、SIGTERM)触发取消,优先用 signal.NotifyContext(Go 1.20+),比“Background + WithCancel + 自己收信号”更简洁。

3. 基本用法示例

package main

import (
	"context"
	"fmt"
	"time"
)

func worker(ctx context.Context, id int) error {
	ticker := time.NewTicker(200 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			// 必须尊重取消
			return ctx.Err()
		case <-ticker.C:
			fmt.Println("doing work", id)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 确保资源释放,哪怕下面提前 return

	go func() {
		if err := worker(ctx, 1); err != nil {
			fmt.Println("worker exit:", err)
		}
	}()

	time.Sleep(1 * time.Second)
	cancel() // 触发所有使用 ctx 的协程退出
	time.Sleep(200 * time.Millisecond)
}

要点:

4. 扇出/扇入与错误快速失败

在并发扇出场景,拿到第一个错误就取消其余任务:

func fetchAll(ctx context.Context, urls []string) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	errCh := make(chan error, len(urls))
	var wg sync.WaitGroup

	for _, u := range urls {
		wg.Add(1)
		go func(u string) {
			defer wg.Done()
			// 你的 I/O 操作必须支持 ctx(HTTP 请求要传 ctx)
			if err := fetchOne(ctx, u); err != nil {
				errCh <- err
				cancel() // 快速失败,通知其他 goroutine 停止
			}
		}(u)
	}

	wg.Wait()
	close(errCh)
	for err := range errCh {
		if err != nil {
			return err
		}
	}
	return nil
}

5. 与WithTimeout/WithDeadline的选择

实践建议:

6. 常见坑与反模式

  1. 忘记调用 cancel()
    即便父 ctx 会被取消,你也应该调用返回的 cancel() 来释放内部计时器/子关系,避免泄漏。
  2. 库函数内部创建根 ctx
    库函数不应 context.Background() 作为根;应当接收调用方传入的 ctx。只有在 main、测试或初始化才创建根。
  3. 协程不检查 ctx.Done()
    导致任务无法停止,程序卡住或泄漏 goroutine。
  4. 把 context 存到结构体字段长期持有
    context 应该显式参数传递到需要的调用链,避免生命周期混乱。
  5. 拿 context.Value 当参数包
    Value 只用于跨 API 边界的请求范围元数据(trace id、auth token),不要当通用参数传递器。

7. 取消语义与错误判断

8. 与外部 I/O 的协作

要让取消生效,外部操作必须接收并使用 ctx。例如:

如果第三方库不支持 ctx,考虑:

9. 实战模式:优雅退出(信号触发)

func main() {
    // 更推荐:signal.NotifyContext
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    g, gctx := errgroup.WithContext(ctx)

    g.Go(func() error { return runHTTPServer(gctx) })
    g.Go(func() error { return runWorkers(gctx) })

    if err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) {
        log.Fatal(err)
    }
}

说明:

10. 简明清单

一个典型的生产场景:优雅关停 HTTP 服务,

确保在收到 SIGTERM/Ctrl+C 后,不再接受新请求,并等待正在处理的请求完成。

1. 背景

HTTP 服务的 http.Server 从 Go 1.8 起支持 Shutdown(ctx) 方法,它会:

  1. 停止监听新连接。
  2. 等待已有连接上的请求完成(直到超时或 ctx 取消)。

我们就可以用 context.WithCancel + 信号监听 来触发这个流程。

2. 示例代码

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	// 1. 创建根 ctx,并能在收到信号时取消
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop() // 释放资源

	// 2. 创建 HTTP server
	mux := http.NewServeMux()
	mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
		// 模拟一个慢请求,且支持 ctx 取消
		select {
		case <-time.After(5 * time.Second):
			fmt.Fprintln(w, "done")
		case <-r.Context().Done():
			// 客户端断开或服务关停时走这里
			log.Println("request canceled:", r.Context().Err())
		}
	})

	srv := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}

	// 3. 启动服务
	go func() {
		log.Println("HTTP server started on :8080")
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("ListenAndServe error: %v", err)
		}
	}()

	// 4. 阻塞等待信号
	<-ctx.Done()
	log.Println("Shutdown signal received")

	// 5. 创建超时 ctx 来优雅关停
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := srv.Shutdown(shutdownCtx); err != nil {
		log.Fatalf("HTTP server Shutdown error: %v", err)
	}
	log.Println("HTTP server exited gracefully")
}

3. 运行流程

  1. 启动程序后,srv.ListenAndServe() 在独立 goroutine 监听请求。

  2. 主 goroutine 通过 <-ctx.Done() 等待信号触发。

  3. 收到 SIGTERM/Ctrl+C 时:

    • signal.NotifyContext 内部调用 cancel() → 主 goroutine 继续执行。
    • 调用 srv.Shutdown(shutdownCtx),阻止新连接,等待已有请求完成。
  4. 如果 10 秒超时未完成,Shutdown 会强制关闭连接。

4. 关键点说明

5. 常见扩展模式

  1. 多服务关停(HTTP + Kafka + gRPC 等)
    把 ctx 传给所有子服务,每个子服务在 ctx.Done() 时执行自己的关停逻辑。
  2. 健康检查 / readiness
    在关停流程里,先修改健康检查状态(例如 /healthz 返回非 200),再执行 Shutdown。
  3. 并发任务收尾
    用 errgroup.WithContext(ctx) 管理后台任务,信号到达时全部取消。

到此这篇关于Golang中Context.WithCancel 的实战指南的文章就介绍到这了,更多相关Golang Context.WithCancel 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文