context.Context是Go语言中独特的设计,在其他编程语言中我们很少见到类似的概念。context.Context深度支持Golang的高并发。
1.Goroutine 和channel。
在理解context包之前,应该首先熟悉Goroutine和Channel,能加深对context的理解。
1.1 Goroutine
Goroutine是一个轻量级的执行线程,多个Goroutine比一个线程轻量,所以管理Goroutine消耗的资源相对更少。Goroutine是Go中最基本的执行单元,每一个Go程序至少有一个Goroutine:主Goroutine。程序启动时会自动创建。为了能更好的理解Goroutine,先来看一看线程Thread与协程Coroutine的概念。
线程(Thread)
线程是一种轻量级进程,是CPU调度的最小单位。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属于一个进程的其他线程共享进程所拥有的全部资源。线程拥有自己独立的栈和共享的堆,共享堆,不共享栈。线 程 的 切 换 一 般 由 操 作 系 统 调 度 \color{red}{线程的切换一般由操作系统调度}线程的切换一般由操作系统调度。
协程(Coroutine)
协程又称为微线程,与子例程一样,协程也是一种程序组建,相对子例程而言,协程更为灵活,但在实践中使用没有子例程那样广泛。和线程类似,共享堆,不共享栈,协 程 的 切 换 一 般 由 程 序 员 在 代 码 中 显 式 控 制 \color{red}{协程的切换一般由程序员在代码中显式控制}协程的切换一般由程序员在代码中显式控制。他避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。Goroutine和其他语言的协程(coroutine)在使用方式上类似,但从字面意义上来看不同(一个是Goroutine,一个是coroutine),再就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。因此Goroutine可以理解为一种Go语言的协程。同时,Gorotine可以运行在一个或多个线程上。
使用示例
package main
import (
"fmt"
)
func Hello() {
fmt.Println("Hello everyBody, I'm WDS")
}
func main() {
go Hello()
fmt.Println("Example")
}
输出结果:
Example
Hello everyBody, I'm WDS
1.3Channel
Channel就是多个Goroutine 之间的沟通渠道。当我们想要将结果或错误,或任何其他类型的信息从一个 goroutine 传递到另一个 goroutine 时就可以使用通道。通道是有类型的,可以是 int 类型的通道接收整数或错误类型的接收错误等。
假设有个 int 类型的通道 ch,如果想发一些信息到这个通道,语法是 ch <- 1,如果想从这个通道接收一些信息,语法就是 var := <-ch。这将从这个通道接收并存储值到 var 变量。
通过改善1.2中的代码片段,证明通道的使用确保了 goroutine 执行完成并将值返回给 main 。
package main
import (
"fmt"
)
func Hello(ch chan int) {
fmt.Println("Hello everyBody, I'm WDS")
ch <- 1
}
func main() {
ch := make(chan int)
go Hello(ch)
<-ch
fmt.Println("Example")
}
输出结果:
Hello everyBody, I'm WDS
Example
2.Context应用场景
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", SayHello) //设置访问的路由
log.Fatalln(http.ListenAndServe(":8080", nil))
}
func SayHello(writer http.ResponseWriter, request *http.Request) {
fmt.Println(&request)
writer.Write([]byte("Hi,New Request Comes"))
}
上述代码每次请求,handler会创建一个Goroutine 为其提供服务、
在真实应用场景中,每个请求对应的Handler,常会启动额外的的goroutine进行数据查询或PRC调用等。这里可以理解为每次请求的业务处理逻辑中,需要多次访问其他服务,而这些服务是可以并发执行的,即主Gorontine内的多个Goroutine并存。而且,当请求返回时,这些额外创建的goroutine需要及时回收。此外,一个请求对应一组请求域内的数据可能会被该请求调用链条内的各goroutine所需要现在对上面的代码添加一点东西,当请求进来时,hanler 创建一个监控goroutinue,这样每隔1s打印一个Hello World。
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", SayHello) //设置访问的路由
log.Fatalln(http.ListenAndServe(":8080", nil))
}
func SayHello(writer http.ResponseWriter, request *http.Request) {
fmt.Println(&request)
go func() {
for range time.Tick(time.Second) {
fmt.Println("Current request is in progress")
}
}()
time.Sleep(2 * time.Second)
writer.Write([]byte("Hi,New Request Comes"))
}
在这里假定需要耗时2s,但在请求2s后返回,我们期望goroutine 在两次打印Hello World后停止。但运行发现,监控goroutine 打印后,其仍然不会结束,会一直打印下去。问题出在创建goroutine后,未对其生命周期作控制,下面我们使用context作一下控制,即监控程序打印前需检测request.Context()是否已经结束,若结束则退出循环,即结束生命周期。
示例代码:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", SayHello) //设置访问的路由
log.Fatalln(http.ListenAndServe(":8080", nil))
}
func SayHello(writer http.ResponseWriter, request *http.Request) {
fmt.Println(&request)
go func() {
for range time.Tick(time.Second) {
select {
case <-request.Context().Done():
fmt.Println("request is outgoing")
return
default:
fmt.Println("Hello World")
}
}
}()
time.Sleep(2 * time.Second)
writer.Write([]byte("Hi,New Request Comes"))
}
输出结果:
0xc00008e008
Hello World
request is outgoing
基于如上需求,context包应用而生。
context包可以提供一个请求从API请求边界到各goroutine的请求域数据传递、取消信号及截至时间等能力。
3.Context详解
在 Go 语言中 context 包允许传递一个 “context” 到程序中。 Context 如超时或截止日期(deadline)或通道,来指示停止运行和返回。例如,如果正在执行一个 web 请求或运行一个系统命令,定义一个超时对生产级系统通常是个好主意。因为,如果依赖的API运行缓慢,不希望在系统上备份(back up)请求,因为它可能最终会增加负载并降低所有请求的执行效率。导致级联效应。这是超时或截止日期 context 派上用场的地方。
3.1设计原理
Go 语言中的每一个请求的都是通过一个单独的 Goroutine 进行处理的,HTTP/RPC 请求的处理器往往都会启动新的 Goroutine 访问数据库和 RPC 服务,我们可能会创建多个 Goroutine 来处理一次请求,而 Context 的主要作用就是在不同的 Goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期。
每一个 Context 都会从最顶层的 Goroutine 一层一层传递到最下层,这也是 Golang 中上下文最常见的使用方式,如果没有 Context,当上层执行的操作出现错误时,下层其实不会收到错误而是会继续执行下去:
当最上层的 Goroutine 因为某些原因执行失败时,下两层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 Context 时,就可以在下层及时停掉无用的工作减少额外资源的消耗:在这里,来看另一个例子:
package main
import (
"context"
"fmt"
// "log"
// "net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go HelloHandle(ctx, 500*time.Millisecond)
select {
case <-ctx.Done():
fmt.Println("Hello Handle", ctx.Err())
}
}
func HelloHandle(ctx context.Context, duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
case <-time.After(duration):
fmt.Println("Hello World with", duration)
}
}
上面的代码,因为过期时间大运处理时间,所以我们有足够的时间处理改请求,输出结果如下:
Hello World with 500ms
Hello Handle context deadline exceeded
HelloHandle函数并没有进入超时的select分支,但是main函数的select却会等待context.Context的超时并打印出Hello Handle context deadline exceeded。如果我们将处理请求的时间增加至2000ms,程序就会因为上下文过期而被终止。
Hello Handle context deadline exceeded
context deadline exceeded
3.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 会返回相同的结果,该方法可以用来传递请求特定的数据;
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
4.总结
1context.Background 只应用在最高等级,作为所有派生 context 的根。
2.context取消是建议性的,这些函数可能需要一些时间来清理和退出。
3.不要把Context放在结构体中,要以参数的方式传递。
4.以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
5.给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
6.Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递。
7.context.Value应该很少使用,它不应该被用来传递可选参数。这使得 API 隐式的并且可以引起错误。取而代之的是,这些值应该作为参数传递。
8.Context是线程安全的,可以放心的在多个goroutine中传递。同一个Context可以传给使用其的多个goroutine,且Context可被多个goroutine同时安全访问。
9.Context 结构没有取消方法,因为只有派生 context 的函数才应该取消 context。