Go语言中context库的基础使用

介绍

本文主要是一个简单的入门使用,具体细节后续文章会详细说明

从名字来看就是一个上下文的意思,通常上下文会跟随你的整个流水线而走,这样方便在每个进程或者协程中都能拿到一些重要信息。比如java的Dubbo框架里面当执行一个Rpc调用时可以在上下文RpcContext存放trace_id,服务提供方接收到后,如果需要它就可以直接在上下文中拿到

在go语言的http库里面,发送一个request请求也可以携带上下文。接收方可以通过request拿到,这样方便做一些超时处理

go语言中的context除了可以传递value 还提供了一些取消,超时操作。如果程序报错需要中断后续操作,请求调用超出了最大响应时间就可以用得上。

一般上下文信息不建议存放在结构体中,通常放到请求参数的第一位,以ctx命名,如果暂时还不知道需要什么类型的上下文,可以使用context包提供的TODO,其实就是一个空context

官方提供了Context接口,有两个默认实现,

var (
  background = new(emptyCtx)
  todo = new(emptyCtx)
)

context我们都把它理解为一个节点树,没有特殊的情况,我们就使用background、todo 这两个作为顶级context,基于这两个context不断衍生出子context,就成了一个context树

官方给我们提供了四个派生函数,都是通过一个父context,派生出一个子context,可以想象经过多重派生之后就成了树

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val any) Context

首先看最简单的 WithValue 可以在不同的context之间传递key-value,下面例子是打印调用来源

WithValue

package main

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

func main() {
	ctx := context.Background()
	ctx = context.WithValue(ctx, "requestSrc", "main")
	go a(ctx)

	time.Sleep(time.Second)
}

func a(ctx context.Context) {
	fmt.Println("a方法调用来源", ctx.Value("requestSrc"))
	ctx = context.WithValue(ctx, "requestSrc", "a")
	go b(ctx)
}

func b(ctx context.Context) {
	fmt.Println("b方法调用来源", ctx.Value("requestSrc"))
}

执行结果

a方法调用来源 main
b方法调用来源 a

Process finished with the exit code 0

WithCancel

如果一个线程同时有多个子协程工作,并且某个子协程或者主协程出现错误后,希望其它协程也停止工作。就可以用到 WithCancel

举个例子,例子并不是很好,但是可以理解怎么使用,假如同时有多个协程不停消费一个topic中的信息,另外有一个协程一直在监控mq的状态,一旦发现mq不可用,停止所有消费者消费。

package main

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

func main() {

	// 自定义错误消息,不使用默认错误信息
	context.Canceled = errors.New("mq 故障")

	ctx, cancel := context.WithCancel(context.Background())

	for i := 0; i < 5; i++ {
		go func(i int) {
			for {
				select {
				case <-ctx.Done():
					fmt.Printf("%s:%s-%d停止消费\n", ctx.Err(), "consumer", i)
					return
				default:
					fmt.Println("消费topic:order中的消息")
					time.Sleep(time.Second)
				}
			}
		}(i)
	}

	// 假设是一个监听mq的协程,如果发现故障就不让其他消费者消费了
	go func() {
		time.Sleep(time.Second * 2)
		// 故障,通知消费者不在消费
		cancel()
	}()

	// 暂时别让程序停止
	time.Sleep(time.Second * 5)
}

执行结果

消费topic:order中的消息
消费topic:order中的消息
消费topic:order中的消息
消费topic:order中的消息
消费topic:order中的消息
消费topic:order中的消息
消费topic:order中的消息
消费topic:order中的消息
消费topic:order中的消息
消费topic:order中的消息
mq 故障:consumer-3停止消费
mq 故障:consumer-4停止消费
mq 故障:consumer-1停止消费
mq 故障:consumer-0停止消费
mq 故障:consumer-2停止消费

Process finished with the exit code 0

WithTimeout WithDeadline

源码中 WithTimeout 就是调用的 WithDeadline,所以我们介绍 WithTimeout 就可以了

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

从方法名可以看出,带有超时时间的上下文。超时处理在很多框架里都是必备的。所以去看很多go的源码基本上就都用到了context,比如gin框架,go中最基本的http库也是使用到了它。所以大家在项目中还是可以放心使用context

我们就以http标准库举个例子。以客户端携带超时时间为例。启动一个http服务,假设处理时间需要2s,那么我们发送两次请求,一次要求1s内返回数据,一次要求3s内返回数据,观察结果

package main

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

func main() {

	// 启动一个http服务
	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		fmt.Println("hello")
		// 设置处理时间2s
		time.Sleep(time.Second * 2)
	})
	go http.ListenAndServe(":8111", nil)

	// 第一个请求携带超时时间1s
	go func() {
		ctx, _ := context.WithTimeout(context.Background(), time.Second)
		request, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:8111", nil)
		client := &http.Client{}
		_, err := client.Do(request)
		if err != nil {
			fmt.Println("请求1 fail", err.Error())
			return
		}
		fmt.Println("请求1 success")
	}()

	// 第二个请求携带超时时间3s
	go func() {
		ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
		request, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:8111", nil)
		client := &http.Client{}
		_, err := client.Do(request)
		if err != nil {
			fmt.Println("请求2 fail", err.Error())
			return
		}
		fmt.Println("请求2 success")
	}()

	// 不让进程退出
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt)
	<-ch
}

执行结果

hello
hello
请求1 fail Get "http://127.0.0.1:8111": context deadline exceeded
请求2 success

到这大家应该清楚go中的标准库context简单使用了,大家也可以根据它提供的Context接口去实现自己满足自己需求的context,一些框架里面都是会自己实现的。后面我们会深入再讲解context


欢迎关注,学习不迷路!

你可能感兴趣的:(golang,golang)