golang context上下文值传递与控制

1. Context详解

在 Go 语言中 context 包允许传递一个 “context” 到程序中。 Context 如超时或截止日期(deadline)或通道,来指示停止运行和返回。例如,如果正在执行一个 web 请求或运行一个系统命令,定义一个超时对生产级系统通常是个好主意。因为,如果依赖的API运行缓慢,不希望在系统上备份(back up)请求,因为它可能最终会增加负载并降低所有请求的执行效率。导致级联效应。这是超时或截止日期 context 派上用场的地方。

1.1. 设计原理

Go 语言中的每一个请求的都是通过一个单独的 Goroutine 进行处理的,HTTP/RPC 请求的处理器往往都会启动新的 Goroutine 访问数据库和 RPC 服务,我们可能会创建多个 Goroutine 来处理一次请求,而 Context 的主要作用就是在不同的 Goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期。

每一个 Context 都会从最顶层的 Goroutine 一层一层传递到最下层,这也是 Golang 中上下文最常见的使用方式,如果没有 Context,当上层执行的操作出现错误时,下层其实不会收到错误而是会继续执行下去:

当最上层的 Goroutine 因为某些原因执行失败时,下两层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 Context 时,就可以在下层及时停掉无用的工作减少额外资源的消耗:

 

1.2. Context接口

context.Context 是 Go 语言在 1.7 版本中引入标准库的接口1,该接口定义了四个需要实现的方法,其中包括:

Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消之后关闭,多次调用 Done 方法会返回同一个 Channel;
Err — 返回 context.Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值;如果 context.Context 被取消,会返回 Canceled 错误;如果 context.Context 超时,会返回 DeadlineExceeded 错误;
Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

Context接口
 // A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
	// Deadline returns the time when work done on behalf of this context
	// should be canceled. Deadline returns ok==false when no deadline is
	// set. Successive calls to Deadline return the same results.
	Deadline() (deadline time.Time, ok bool)

	// Done returns a channel that's closed when work done on behalf of this
	// context should be canceled. Done may return nil if this context can
	// never be canceled. Successive calls to Done return the same value.
	// The close of the Done channel may happen asynchronously,
	// after the cancel function returns.
	//
	// WithCancel arranges for Done to be closed when cancel is called;
	// WithDeadline arranges for Done to be closed when the deadline
	// expires; WithTimeout arranges for Done to be closed when the timeout
	// elapses.
	//
	// Done is provided for use in select statements:
	//
	//  // Stream generates values with DoSomething and sends them to out
	//  // until DoSomething returns an error or ctx.Done is closed.
	//  func Stream(ctx context.Context, out chan<- Value) error {
	//  	for {
	//  		v, err := DoSomething(ctx)
	//  		if err != nil {
	//  			return err
	//  		}
	//  		select {
	//  		case <-ctx.Done():
	//  			return ctx.Err()
	//  		case out <- v:
	//  		}
	//  	}
	//  }
	//
	// See https://blog.golang.org/pipelines for more examples of how to use
	// a Done channel for cancellation.
	Done() <-chan struct{}

	// If Done is not yet closed, Err returns nil.
	// If Done is closed, Err returns a non-nil error explaining why:
	// Canceled if the context was canceled
	// or DeadlineExceeded if the context's deadline passed.
	// After Err returns a non-nil error, successive calls to Err return the same error.
	Err() error

	// Value returns the value associated with this context for key, or nil
	// if no value is associated with key. Successive calls to Value with
	// the same key returns the same result.
	//
	// Use context values only for request-scoped data that transits
	// processes and API boundaries, not for passing optional parameters to
	// functions.
	//
	// A key identifies a specific value in a Context. Functions that wish
	// to store values in Context typically allocate a key in a global
	// variable then use that key as the argument to context.WithValue and
	// Context.Value. A key can be any type that supports equality;
	// packages should define keys as an unexported type to avoid
	// collisions.
	//
	// Packages that define a Context key should provide type-safe accessors
	// for the values stored using that key:
	//
	// 	// Package user defines a User type that's stored in Contexts.
	// 	package user
	//
	// 	import "context"
	//
	// 	// User is the type of value stored in the Contexts.
	// 	type User struct {...}
	//
	// 	// key is an unexported type for keys defined in this package.
	// 	// This prevents collisions with keys defined in other packages.
	// 	type key int
	//
	// 	// userKey is the key for user.User values in Contexts. It is
	// 	// unexported; clients use user.NewContext and user.FromContext
	// 	// instead of using this key directly.
	// 	var userKey key
	//
	// 	// NewContext returns a new Context that carries value u.
	// 	func NewContext(ctx context.Context, u *User) context.Context {
	// 		return context.WithValue(ctx, userKey, u)
	// 	}
	//
	// 	// FromContext returns the User value stored in ctx, if any.
	// 	func FromContext(ctx context.Context) (*User, bool) {
	// 		u, ok := ctx.Value(userKey).(*User)
	// 		return u, ok
	// 	}
	Value(key any) any
}

2. 实例

2.1.取消上下文的控制context.WithCancel

go代码如下,控制一个吃汉堡函数调用的结束:

结束调试是个数》=10的时候

控制函数调用的结束
 package main

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

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	eatNum := chiHanBao(ctx)

	for n := range eatNum {
		if n>=10{
			cancel()
			break
		}
		fmt.Print("n,", n)
		time.Sleep(1 * time.Second)
	}


	fmt.Println("正在统计结果。。。")
	time.Sleep(1 * time.Second)
}

func chiHanBao(ctx context.Context) <-chan int {
	c := make(chan int)
	// 个数
	n := 0
	// 时间
	t := 0
	go func() {
		for {
			select {
			case <-ctx.Done():
				fmt.Printf("耗时 %d 秒,吃了 %d 个汉堡 \n", t, n)
				return
			case c <- n:
				incr := 1
				n += incr
				t++
				fmt.Printf("我吃了 %d 个汉堡\n", n)
			}
		}
	}()

	return c
}

输出:

n,0我吃了 1 个汉堡
n,1我吃了 2 个汉堡
n,2我吃了 3 个汉堡
n,3我吃了 4 个汉堡
n,4我吃了 5 个汉堡
n,5我吃了 6 个汉堡
n,6我吃了 7 个汉堡
n,7我吃了 8 个汉堡
n,8我吃了 9 个汉堡
n,9我吃了 10 个汉堡
正在统计结果。。。
我吃了 11 个汉堡
耗时 11 秒,吃了 11 个汉堡 
Exiting.

 

 

2.2.超时上下文的控制context.WithTimeout 

go代码如下,控制一个吃汉堡函数调用的结束:

控制吃汉堡结束时机:10秒以后
package main

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

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	chiHanBao2(ctx)
	defer cancel()
}

func chiHanBao2(ctx context.Context) {
	n := 0
	for {
		select {
		case <-ctx.Done():
			fmt.Println("stop \n")
			return
		default:
			n += 1
			fmt.Printf("我吃了 %d 个汉堡\n", n)
		}
		time.Sleep(time.Second)
	}
}

效果:

我吃了 1 个汉堡
我吃了 2 个汉堡
我吃了 3 个汉堡
我吃了 4 个汉堡
我吃了 5 个汉堡
我吃了 6 个汉堡
我吃了 7 个汉堡
我吃了 8 个汉堡
我吃了 9 个汉堡
我吃了 10 个汉堡
stop 

 

2.3.期限结束上下文的控制context.WithDeadline

go代码如下,控制一个吃汉堡函数调用的结束:

停止吃汉堡的期限在10秒后
 package main

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

func main() {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
	chiHanBao2(ctx)
	defer cancel()
}

func chiHanBao2(ctx context.Context) {
	n := 0
	for {
		select {
		case <-ctx.Done():
			fmt.Println("stop \n")
			return
		default:
			incr := 1
			n += incr
			fmt.Printf("我吃了 %d 个汉堡\n", n)
		}
		time.Sleep(time.Second)
	}
}
我吃了 1 个汉堡
我吃了 2 个汉堡
我吃了 3 个汉堡
我吃了 4 个汉堡
我吃了 5 个汉堡
我吃了 6 个汉堡
我吃了 7 个汉堡
我吃了 8 个汉堡
我吃了 9 个汉堡
我吃了 10 个汉堡
stop 

2.4.上下文值的传递

通过上下文值的传递,可以捕捉整个调用栈:

函数间通过context传递值
 package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.WithValue(context.Background(), "trace_id", "88888888")
	// 携带session到后面的程序中去
	ctx = context.WithValue(ctx, "session", 1)

	fmt.Print("ctx, string", ctx)

	process(ctx)
}

func process(ctx context.Context) {
	session, ok := ctx.Value("session").(int)
	fmt.Println(ok)
	if !ok {
		fmt.Println("something wrong")
		return
	}

	if session != 1 {
		fmt.Println("session 未通过")
		return
	}

	traceID := ctx.Value("trace_id").(string)
	fmt.Println("traceID:", traceID, "-session:", session)
}

效果:

traceID: 88888888 -session: 1

 

posted @ 2023-02-03 18:26  若-飞  阅读(1189)  评论(0编辑  收藏  举报