context在Go并发编程中的定位
Go语言的context包用于在goroutine之间传递截止时间、取消信号和请求作用域的值。在微服务架构中,一个HTTP请求可能衍生多个goroutine调用下游服务,context提供了一种自上而下的取消传播机制,避免goroutine泄漏。
context的核心接口:
type Context interface {
Deadline() (deadline time.Time, ok bool) // 返回截止时间
Done() <-chan struct{} // 返回取消信号channel
Err() error // 返回取消原因
Value(key any) any // 获取请求作用域的值
}
Done()返回一个channel,当context被取消或超时时间到达时,该channel被关闭。在select语句中监听Done()是标准的取消模式。
context的创建与传播
context的创建遵循树形结构,从父context派生子context。四种创建函数:
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 1. 根context
ctx := context.Background()
// 2. 带超时的context
ctxTimeout, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// 3. 带截止时间的context
ctxDeadline, cancel2 := context.WithDeadline(ctx, time.Now().Add(10*time.Second))
defer cancel2()
// 4. 带值的context
ctxValue := context.WithValue(ctx, "requestID", "abc-123")
// 5. 可手动取消的context
ctxCancel, cancel3 := context.WithCancel(ctx)
defer cancel3()
go worker(ctxTimeout, "worker-1")
go worker(ctxCancel, "worker-2")
time.Sleep(6 * time.Second)
}
func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s 取消, 原因: %v\n", name, ctx.Err())
return
case <-time.After(1 * time.Second):
fmt.Printf("%s 工作中...\n", name)
}
}
}
每个WithXxx函数返回一个context和一个cancel函数。cancel函数必须被调用,否则会泄漏context关联的资源。使用defer cancel()是标准做法。即使goroutine正常退出,也要调用cancel,因为context会持有定时器等资源直到被取消或超时。
HTTP服务中的context超时控制
Go标准库的http.Request自带context,可从请求中获取并传递给下游调用:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
type ServiceClient struct {
client *http.Client
}
func (s *ServiceClient) CallAPI(ctx context.Context, url string) error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := s.client.Do(req)
if err != nil {
if ctx.Err() != nil {
return fmt.Errorf("请求被取消: %w", ctx.Err())
}
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("状态码: %d", resp.StatusCode)
}
return nil
}
func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
client := &ServiceClient{
client: &http.Client{Timeout: 10 * time.Second},
}
err := client.CallAPI(ctx, "http://downstream-service/api")
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "请求超时", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, "OK")
}
r.Context()返回的context在客户端断开连接时自动取消。在其基础上派生超时context,实现客户端断开加超时双重保护。http.NewRequestWithContext将context绑定到请求,当context取消时正在执行的HTTP请求会被中断。
goroutine泄漏的常见模式与排查
goroutine泄漏是指goroutine因阻塞在channel或锁上永远无法退出,导致内存和CPU资源持续占用。最典型的泄漏模式:启动goroutine但未传递context,channel发送端或接收端缺失。
// 泄漏示例1:无超时的channel等待
func leakyFunc() {
ch := make(chan int)
go func() {
val := <-ch // 永远阻塞,如果没人往ch发送数据
fmt.Println(val)
}()
// 函数返回后,goroutine仍在等待
}
// 泄漏示例2:HTTP handler启动goroutine未控制生命周期
func handler(w http.ResponseWriter, r *http.Request) {
go func() {
// r.Context()在handler返回后取消
// 但这个goroutine可能仍在使用r相关资源
processBackground(r.Context(), r)
}()
w.Write([]byte("accepted"))
}
排查goroutine泄漏的方法:使用runtime/pprof获取goroutine堆栈信息:
import (
"os"
"runtime/pprof"
)
func dumpGoroutines() {
f, _ := os.Create("goroutine.dump")
defer f.Close()
pprof.Lookup("goroutine").WriteTo(f, 1)
}
// 或通过HTTP端点暴露
import _ "net/http/pprof"
// 访问 http://localhost:6060/debug/pprof/goroutine?debug=1
// 查看所有goroutine的调用栈
分析goroutine dump时,关注数量异常多的相同调用栈,通常指向泄漏点。每个goroutine堆栈包含创建位置(created by …)和当前阻塞位置(blocked on …),两者结合定位泄漏源。
errgroup实现并发任务管理
golang.org/x/sync/errgroup包在context基础上提供并发goroutine编排,自动处理错误传播和取消:
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"net/http"
)
func fetchAll(ctx context.Context, urls []string) ([]*http.Response, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]*http.Response, len(urls))
for i, url := range urls {
i, url := i, url // 捕获循环变量
g.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
results[i] = resp
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
g.Go启动的goroutine中任一返回error,errgroup会取消ctx,其他goroutine通过ctx.Done()感知取消并退出。g.Wait()等待所有goroutine完成,返回第一个非nil错误。这种模式在并发调用多个下游服务的场景中非常实用,一个服务失败时自动取消其他无关请求。
errgroup.SetLimit(n)限制最大并发goroutine数量,防止一次性启动过多goroutine耗尽资源。结合semaphore机制实现并发控制,比手动管理channel信号量更简洁可靠。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-context-shang-xia-wen-chao-shi-kong-zhi-yu/