golang学习之上下文(context)初探

1,082 阅读3分钟

前言 在web系统中,每次的网络请求都会创建一个协程或者线程去处理请求,在每一个请求中基本上都会另外开多个协程去请求对应的数据资源, 那如何的进行协程间的等停和数据传递呢,这里就用到了下文要提到的context了也就是上下文对象,其实在大部分的语言中都没有context的概念。

what is context?

官方:A Context carries a deadline, a cancellation signal, and other values across

什么意思呢?

context是一个带有截止时间,取消信号和key,value的上下文对象。

如何使用

下面我们来看个示例 1:

func main() {
	fmt.Println("main is start")
	go Test("value")
	fmt.Println("main is finish")
	time.Sleep(3 * time.Second)
}

func Test(value string) {
	fmt.Println("test is start")
	time.Sleep(5 * time.Second)
	fmt.Println("test is finish")
}

翠花上结果: 在示例1中我们在创建一个协程Test,我们利用time.Sleep方法去模拟Test执行时间, 但是在主协程中我们是没办法去获取子协程的状态的,同时我们也办法在子协程了解到主协程的运行状态以及是否完结,当主协程执行完毕或者done掉了,那么子协程还在运行,这样其实是十分浪费资源的。所以在这个时候,context就派上了用场。

同步信号

package main

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

func main() {
	fmt.Println("main is start")
	ctx, _ := context.WithTimeout(context.Background(), 1*time.Second)
	go TestContext(ctx, 1500*time.Millisecond)
	fmt.Println("main is finish")
	select {
	case <-ctx.Done():
		fmt.Println("[main] is deadline", ctx.Err())
	}
}

// 测试Context的unc
func TestContext(context context.Context, timSec time.Duration) {
	fmt.Println("testContext is start")
	select {
	case <-context.Done():
		fmt.Println("[TestContext] is err", context.Err())
	case <-time.After(timSec):
		fmt.Println("[TestContext] is with time ", timSec)
	}
}

翠花上结果: 可以看到我们在主协程中调用Context包的WithTimeOut方法去设置一个超时时间同时返回一个新的Context 稍后我们回去分析context的结构和方法。在栗子中我们可以看到子协程并没有走到超时的逻辑中,但是主协程却会因为等待超时而进去select的Done()逻辑里。 我们稍微改动下将子协程的超时时间改为500毫秒,

	go TestContext(ctx, 1500*time.Microsecond)

翠花上图片 这里我们发现子协程内由于执行时间过长,直接发生了超时异常,同样的我们的主协程内同样也超时了。

context 传值

func main() {
	fmt.Println("main is start")
	ctx := context.WithValue(context.Background(), "key", "value")
	go TestValue(ctx)
	time.Sleep(1 * time.Second)
}

func TestValue(ctx context.Context) {
	fmt.Println("========>", ctx.Value("key"))
	select {
	case <-ctx.Done():
		fmt.Println("[TestContext] and value is ", ctx.Value("key"))
	}
}

翠花上图片

我们使用context的withValue()方法去创建一个context并且将传入value值。 我们可以看到在子协程中可以ctx获取到key的值。我们稍微改造下代码

func TestValue(ctx context.Context) {
	fmt.Println("TestValue ========01========", ctx.Value("key"))
	go func() {
		fmt.Println("TestValue ========02========", ctx.Value("key"))
	}()
}

翠花上截图 我们在子协程中再创建一个子协程,很明显的我们可以看到两个协程的ctx的key值都是一样的。这其实也说明了context在协程中的传递

centext的源码

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

context是一个接口其中包含了四个方法DeadLine(), Done(), Err(), Value(),其实在刚才的使用上我们使用了下面三个方法,Done()的返回值是一个空的channel,当发生context超时时会执行其,并且Err()方法会打印对应的错误,而value方法其实是传递了自上而下的value值。 Deadline()返回代表该上下文完成工作的时间.代表了该context需要取消的时间。 示例:

ctx := context.WithValue(context.Background(), "key", "value")

在上面的实例中我们使用WithValue()存储一个key,value值,通知返回一个新的context对象,而传递的context.Backgroud() 返回的context包是最简答的方法:

	type emptyCtx int
	background = new(emptyCtx)
}

而这个结构的方法是 never canceled, has no values, and has no deadline

func TODO() Context {
	return todo
}

func Background() Context {
	return background
}

上述的函数中返回的都是emptyCtx 这个结构的方法。

总结

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

最后

学无止境