golang-Context包之Context

简介

Context意思为”上下文”,主要的作用是用来在goroutine之间传递上下文的信息。上下文包含的功能有:

  • k-v (WithValue)
  • 取消信号(withCancel)
  • 超时时间(WithTimeout)
  • 截止时间(WithDeadline)

上下文是可以进行传播的,也就是说通过调用一些方法可以由父级派生出子级

使用场景

Context的功能可以从两个方面去体现:

  1. 发送终止信号去提供给方法
  2. 传递一些公用的数据给多个方法

为什么需要取消操作(终止信号)

简单来说,我们需要一些终止信号,来告诉我们的程序去停止进行一些不必要的工作。
用一个简单的例子来说明:

Client Application DB Request DB Query Client Application DB

从客户端发送请求到程序处理再到数据库中,正常流程如下图所示

Client Application DB Request Return Response Send DB Query Return DB Result Client Application DB

但是当客户端发送请求后,如果断开链接,那么正常情况下后序的数据库查询操作应该停止,如果没有任何中止信号那么程序会继续执行,但是正常的情况是需要终止继续操作

Client Application DB Request Pending Send DB Query Client Application DB

所以我们就知道为什么需要终止信号了

package main

import (
	"fmt"
	"net/http"
	"os"
	"time"
	"context"
)
type ContextKey string
func main() {
	// 测试方法
	// 浏览器打开localhost:8000
	// 两秒之后会返回request processed
	// 如果发送请求后立刻断开那么 ctx.Done() 能够收到信号 执行中断
	http.HandleFunc("/",handlerOne)
	//测试timeout和cancel
	http.HandleFunc("/timeout",handlerTwo)
	http.ListenAndServe(":8000", nil)
}

func handlerOne(w http.ResponseWriter, r *http.Request)  {
	ctx := r.Context()
	fmt.Fprint(os.Stdout, "processing request\n")

	select {
	case <-time.After(2 * time.Second):
		// 2秒后要是收到信号
		// 说明请求被执行
		w.Write([]byte("request processed"))
	case <-ctx.Done():
		// 如果请求被取消 输出请求被取消
		fmt.Fprintln(os.Stderr, "request cancelled")
		fmt.Println(ctx.Err())
	}
}

func handlerTwo(w http.ResponseWriter, r *http.Request)  {
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	ctx = context.WithValue(ctx, ContextKey("Ethan"), "Hi Leo")
	defer cancel()
	c := make(chan string)
	go func() {
		getFromDb(ctx, c)
	}()

	select {
	case <-ctx.Done():
		w.Write([]byte(ctx.Err().Error()))
	case data := <-c:
		w.Write([]byte(data))

	}

}

func getFromDb(ctx context.Context, c chan<- string) {
	time.Sleep(6 * time.Second)
	value := ctx.Value(ContextKey("Ethan"))
	v, ok := value.(string)
	if !ok {
		c <- "get data error"
		return
	}
	c <- v
}

方法

func Background() Context

ctx:=context.Background()

该方法返回一个非零的空上下文。它永远不会被取消,没有价值,也没有截止日期。它通常由 main 函数、初始化和测试使用,并作为传入请求的顶级上下文。

func TODO() Context

ctx:=context.TODO()

该方法返回一个非零的空上下文。如果实在不清楚需要传递什么,可以使用该方法

func WithValue(parent Context, key, val any) Context

func WithValue(parent Context, key, val interface{}) Context 1.18以前的版本函数签名是这样的
官方建议尽量别使用内置类型
The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys. To avoid allocating when assigning to an interface{}, context keys often have concrete type struct{}. Alternatively, exported context key variables’ static type should be a pointer or interface.

该方法会返回一个context的副本,返回的ctx中会携带key,val的信息

type contextKey string
ctx:=context.Background()
k:=contextKey("Ethan")
ctx = context.WithValue(ctx,k,"Leo")

示例

type contextKey string
func WithValue() {
	k := contextKey("Ethan")
	ctx := context.Background()
	f(ctx)
	ctx = context.WithValue(ctx, k, "Leo")
	f(ctx)
}
func f(ctx context.Context) {
	if value := ctx.Value(contextKey("Ethan")); value != nil {
		fmt.Printf("value is %v\n", value)
		return
	}
	fmt.Println("no value")
}
//$ go test -v -run TestWithValue
//=== RUN   TestWithValue
//=== RUN   TestWithValue/test_with_value
//no value
//value is Leo

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

该方法会返回一个context副本,和一个取消函数,参数timeout的设置,会让ctx在指定的时间段时候,接收到ctx.Done()的信号,用来关闭context

var c chan struct{}
func WithTimeOut() {
	c = make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	go f(ctx)
	time.Sleep(6 * time.Second)
	//time.Sleep(1 * time.Second)
	close(c)
	time.Sleep(1 * time.Second)

}
func f(ctx context.Context) {
	select {
	case <-c:
		fmt.Println("main goroutine is done")
	case <-ctx.Done():
		fmt.Println("context is time out")
	}
}

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

WithDeadline 返回父上下文的副本,截止日期调整为不晚于 d。如果父级的截止日期已经早于 d,则 WithDeadline(parent, d) 在语义上等同于父级。当截止日期到期、调用返回的取消函数或父上下文的 Done 通道关闭时,返回的上下文的 Done 通道将关闭,以先发生者为准

示例

package with_daedline

import (
	"context"
	"fmt"
	"time"
)
func WithDeadLine() {
	deadline := time.Now().Add(10 * time.Second)
	ctx, cancel := context.WithDeadline(context.Background(), deadline)
	defer cancel()
	c := make(chan string)

	go readFile(c)
	go getDb(c)

	select {
	case <-ctx.Done():
		fmt.Println(ctx.Err().Error())
	case data := <-c:
		fmt.Println(data)

	}
}

func readFile(c chan<- string) {
	time.Sleep(11 * time.Second)
	c <- "data from file"
}

func getDb(c chan<- string) {
	time.Sleep(13 * time.Second)
	c <- "data from db"
}

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

  1. WithCancel()函数接受一个 Context 并返回其子Context和取消函数cancel
  2. 新创建协程中传入子Context做参数,且需监控子Context的Done通道,若收到消息,则退出
  3. 需要新协程结束时,在外面调用 cancel 函数,即会往子Context的Done通道发送消息
  4. 注意:当 父Context的 Done() 关闭的时候,子 ctx 的 Done() 也会被关闭

示例

  1. 利用根Context创建一个父Context,使用父Context创建一个协程,
  2. 利用上面的父Context再创建一个子Context,使用该子Context创建一个协程
  3. 一段时间后,调用父Context的cancel函数,会发现父Context的协程和子Context的协程都收到了信号,被结束了
func WithCancelTwo() {
	ctx, cancel := context.WithCancel(context.Background())
	// 父context的子协程
	go watch1(ctx)
	// 子context,注意:这里虽然也返回了cancel的函数对象,但是未使用
	valueCtx, _ := context.WithCancel(ctx)
	// 子context的子协程
	go watch2(valueCtx)
	fmt.Println("现在开始等待3秒,time=", time.Now().Unix())
	time.Sleep(3 * time.Second)
	// 调用cancel()
	fmt.Println("等待3秒结束,调用cancel()函数")
	cancel()
	// 再等待5秒看输出,可以发现父context的子协程和子context的子协程都会被结束掉
	time.Sleep(5 * time.Second)
	fmt.Println("最终结束,time=", time.Now().Unix())
}
// 父context的协程
func watch1(ctx context.Context) {
	for {
		select {
		case <-ctx.Done(): //取出值即说明是结束信号
			fmt.Println("收到信号,父context的协程退出,time=", time.Now().Unix())
			return
		default:
			fmt.Println("父context的协程监控中,time=", time.Now().Unix())
			time.Sleep(1 * time.Second)
		}
	}
}
// 子context的协程
func watch2(ctx context.Context) {
	for {
		select {
		case <-ctx.Done(): //取出值即说明是结束信号
			fmt.Println("收到信号,子context的协程退出,time=", time.Now().Unix())
			return
		default:
			fmt.Println("子context的协程监控中,time=", time.Now().Unix())
			time.Sleep(1 * time.Second)
		}
	}
}
//$ go test -v -run TestWithCancelTwo
//=== RUN   TestWithCancelTwo
//=== RUN   TestWithCancelTwo/test_with_cancel
//现在开始等待3秒,time= 1671882383
//父context的协程监控中,time= 1671882383
//子context的协程监控中,time= 1671882383
//子context的协程监控中,time= 1671882384
//父context的协程监控中,time= 1671882384
//父context的协程监控中,time= 1671882385
//子context的协程监控中,time= 1671882385
//子context的协程监控中,time= 1671882386
//等待3秒结束,调用cancel()函数
//父context的协程监控中,time= 1671882386
//收到信号,子context的协程退出,time= 1671882387
//收到信号,父context的协程退出,time= 1671882387
//最终结束,time= 1671882391

你可能感兴趣的:(#,每日一库,Golang,golang)