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