0%

Go Context 使用场景

context 使用场景

1 超时控制

实际使用中, context 经常和 select 关键字一起使用. 用于监听 context 结束 取消.

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

func testTimeout() {
	timerCtx, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancelFunc()

	useContext(timerCtx)
}

func useContext(ctx context.Context) {
	ch := make(chan struct{})
	go func() {
		processLogic()
		ch <- struct{}{}
	}()
	//process
	select {
	case <-ctx.Done():
		fmt.Println("上下文取消 or 超时.")
	case <-ch:
		fmt.Println("process log  succeess.")
	}
}

func processLogic() {
	time.Sleep(time.Second * 2)
}

其中 timerContext 是可以 在 上下文链路中传递, 当上游配置 timeout, 调用 下游 服务时, 如果 超时 , 可以通过 context 级联取消下游服务的处理, 避免下游服务占用资源.
(已经超时了, 还在处理业务)

2. Request Scope 传递共享数据

比如 在 http 请求中, 传递 traceID, userID…


import (
	"context"
	"fmt"
	"net/http"
)

type requestKeyType string

// 避免冲突. 
var requestIdKey = requestKeyType("requestID")

func initHttp() {
	h := WithRequestID(Handle)
	err := http.ListenAndServe(":8080", h)
	if err != nil {
		panic(err)
	}
}

func WithRequestID(next http.HandlerFunc) http.Handler {
	return http.HandlerFunc(
		func(w http.ResponseWriter, r *http.Request) {
			// 从header 中提取 requestID
			requestID := r.Header.Get("X-Request-ID")
			valCtx := context.WithValue(r.Context(), requestIdKey, requestID)
			// 构建 新的请求
			r = r.WithContext(valCtx)
			//调用 http处理函数
			next.ServeHTTP(w, r)
		},
	)
}

func Handle(writer http.ResponseWriter, request *http.Request) {
	requestID, ok := request.Context().Value(requestIdKey).(string)
	if !ok {
		return
	}
	fmt.Println(requestID)
	writer.Write([]byte(requestID))
}

3. tracing 组件中 carrier SpanContext

context 注意事项

- 对第三方调用要传入 context, 用于控制远程调用
- 不要将上下文存储在结构类型中,尽可能的作为函数第一位形参传入.
- 函数调用链必须传播上下文,实现完整链路上的控制
- context 的继承和派生, 保证父、子级 context 的联动
- 不传递 nil context,不确定的 context 应当使用 TODO
- context 仅传递必要的值, 不要让可选参数揉在一起

1. 对第三方调用要传入 context, 用于控制远程调用

在 golang 中 对 context 的 使用 已经是 约定俗成的规定, 因此 使用第三方 服务的时候, 要传入 context.


import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func httpTimeout() {
	req, err := http.NewRequest("GET", "https://google.com", nil)
	if err != nil {
		panic(err)
	}

	timerCtx, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancelFunc()

	req = req.WithContext(timerCtx)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Printf("http.DefaultClient.Do error, err = %v \n", err)
		return
	}
	defer resp.Body.Close()
}

这样子由于第三方开源库已经实现了根据 context 的超时控制,那么当你所传入的时间到达时,将会中断调用.
若你发现第三方开源库没支持 context, 那就 看看 是否要使用,否则出问题 没有 简单的控制手段.

2. 不要将上下文存储在结构类型中,尽可能的作为函数第一位形参传入.

在 golang 中, 所有的第三方库, 开源代码. 清一色的 会将context 作为方法的第一个参数. 并且命名为 ctx.
标准要求: context 作为方法的第一个参数,并且命名为 ctx

当然也有极少数情况 会将 context 放在结构体里面,基本常见于:

  • DDD 架构
  • 底层基础库

每个请求都是独立的,context也就不一样,想清楚业务场景最重要.否则 遵循 go基本规范就好.

3. Trace 函数调用链必须传播上下文,实现完整链路上的控制

把 context 作为方法第一个参数,本质是为了 传播 context, 完成调用链路的跟踪和 控制.

import (
	"context"
	
	"github.com/jmoiron/sqlx"
	"github.com/opentracing/opentracing-go"
	"github.com/pkg/errors"
)

type User struct {
}

func List(ctx context.Context, db *sqlx.DB) ([]User, error) {
	span, ctx := opentracing.StartSpanFromContext(ctx, "internal.user.List")
	defer span.Finish()

	users := []User{}
	const q = `SELECT * FROM users`

	if err := db.SelectContext(ctx, &users, q); err != nil {
		return nil, errors.Wrap(err, "selecting users")
	}

	return users, nil
}

4. context 的继承和派生, 保证父、子级 context 的联动

ctx := context.Background()
ctx1, _ := context.WithCancel(ctx)
ctx2, _ := context.WithDeadline(ctx, time.Now().Add(time.Second))
ctx3, _ := context.WithTimeout(ctx, time.Second)
ctx4 := context.WithValue(ctx, "key", "value")
ctx5 := conetext.WithValue(ctx2, "key", "value")

context Value的查找是 回溯树的方式.(由下至上)
cancel 一个节点, 会 cancel其所有子节点.(由上至下)

5. 不传递 nil context,不确定的 context 应当使用 TODO

在实际使用 context中,对于不知道使用什么类型的 context的时候,
使用 context.TODO() 代替,直到了解清楚 context 的实际用途, 在进行替换.

$$

6. context 仅传递必要的值, 不要让可选参数揉在一起

我们在使用 context 作为上下文时,经常有信息传递的诉求.
像是在 gRPC 中就会有 metadata 的概念, 而在 gin 中就会自己封装 context 作为参数管理.


[参考]
分享 Go 使用 Context 的正式姿势
Contex 的作用
Go Concurrency Patterns: Context

欢迎关注我的其它发布渠道