

什么是 Context
Go 1.7 标准库引入 Context(上下文) ,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。Context 主要用来在 goroutine 之间传递上下文信息。Context 几乎成为了并发控制和超时控制的标准做法。
Context 接口定义如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
可以看到 Context 接口共有 4 个方法:
-
Deadline()
:返回的第一个值是 context 的截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是布尔值, true
表示设置了截止时间, false
表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context 。 -
Done()
:返回一个只读的通道(只有在被 cancel 后才会返回),类型为 struct{}
。在子协程里读这个 channel ,除非被关闭,否则读不出任何东西,根据这一点,就可以做一些清理动作,退出 goroutine 。 -
Err()
:返回 context 被 cancel 的原因。例如是被取消,还是超时。 -
Value()
:返回被绑定到 Context 的值,是一个键值对,所以要通过一个 Key 才可以获取对应的值,这个值一般是线程安全的。
上面这些方法都是用于读取的,不能进行设置。


使用 Context 控制协程
一个协程开启后,我们无法强制关闭,一般关闭协程的原因有以下几种:
- 协程执行完正常退出
- 主协程退出,子协程被迫退出
- 通过信道发送信号,引导协程关闭
下面是一个使用信道控制协程的例子:
package main
import (
"fmt"
"time"
)
func main() {
// 定义 chan
c := make(chan bool)
go func() {
for {
select {
case <- c:
fmt.Println("监控退出,停止了...")
return
default:
fmt.Println("监控子协程中...")
time.Sleep(2 * time.Second)
}
}
}()
time.Sleep(10 * time.Second)
fmt.Println("通知监控停止")
c <- true
// 为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
上面的程序中,我们定义了一个 chan ,通过该 chan 引导子协程关闭。开启子协程后,我们让主协程休眠 10s ,子协程不断循环,使用 select
判断 chan 是否可以接收到值,如果可以接收到,则退出子协程;否则执行 default
继续监控,直到接收到值为止。该程序运行后输出如下:
监控子协程中...
监控子协程中...
监控子协程中...
监控子协程中...
监控子协程中...
通知监控停止
监控退出,停止了...
这证明了我们可以通过信道引导协程的关闭。
当然,使用信道可以控制多个协程,下面是使用一个信道控制多个子协程的例子:
package main
import (
"fmt"
"time"
)
func monitor(c chan bool, num int) {
for {
select {
case value := <- c:
fmt.Printf("监控器%v 接收值%v 监控结束\n", num, value)
return
default:
fmt.Printf("监控器%v 监控中...\n", num)
time.Sleep(2 * time.Second)
}
}
}
func main() {
c := make(chan bool)
for i := 0; i < 3; i++ {
go monitor(c, i)
}
time.Sleep(time.Second)
// 关闭所有的子协程
close(c)
// 为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
fmt.Println("主程序退出")
}
上面的代码中,我们使用 close
关闭通道后,如果该通道是无缓冲的,则它会从原来的阻塞变成非阻塞,也就是可读的,不过读到的会一直是零值,因此根据这个特性就可以判断拥有该通道的协程是否要关闭。运行该程序输出如下:
监控器0 监控中...
监控器1 监控中...
监控器2 监控中...
监控器2 接收值false 监控结束
监控器0 接收值false 监控结束
监控器1 接收值false 监控结束
主程序退出
到这里,我们一直讲的是使用信道控制协程,还没提到 Context 呢。那么既然能用信道控制协程,为什么还要用 Context 呢?因为使用 Context 更好用而且更优雅,下面是基于上面例子使用 Context 控制协程的代码:
package main
import (
"context"
"fmt"
"time"
)
func monitor(con context.Context, num int) {
for {
select {
// 判断 con.Done() 是否可读
// 可读就说明该 context 已经取消
case value := <- con.Done():
fmt.Printf("监控器%v 接收值%v 监控结束\n", num, value)
return
default:
fmt.Printf("监控器%v 监控中...\n", num)
time.Sleep(2 * time.Second)
}
}
}
func main() {
// 为 parent context 定义一个可取消的 context
con, cancel := context.WithCancel(context.Background())
for i := 0; i < 3; i++ {
go monitor(con, i)
}
time.Sleep(time.Second)
// 关闭所有的子协程
// 取消 context 的时候,只要调用一下 cancel 方法即可
// 这个 cancel 就是在创建 con 的时候返回的第二个值
cancel()
// 为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
fmt.Println("主程序退出")
}
运行该程序输出如下:
监控器2 监控中...
监控器0 监控中...
监控器1 监控中...
监控器0 接收值{} 监控结束
监控器2 接收值{} 监控结束
监控器1 接收值{} 监控结束
主程序退出
可以看到和上面使用信道控制协程的效果相同。


根 Context
创建 Context 必须要指定一个父 Context,创建第一个 Context 时,指定的父 Context 是 Go 中已经实现的 Context ,Go 中内置的 Context 有以下两个:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
可以看到,其中一个内置的 Context 是 Background
,其主要用于 main 函数、初始化以及测试代码中,作为 Context 这个树结构的最顶层的 Context ,也就是根 Context ,它不能被取消。
而另一个内置的 Context 是 TODO
,当你不知道要使用什么 Context 的时候就可以使用这个,但其实看源码和上面那个内置的 Context 只是名称不一样罢了,一般使用上面那个。
再者,我们看到内置的 Context 都是 emptyCtx
结构体类型,查看源码不难发现,它是一个不可取消,没有设置截止时间,没有携带任何值的 Context 。
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}


Context API
context 包有几个 With 系列的函数,它们可以可以返回新 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 interface{}) Context
以上函数都有一个共同点,第一个参数都接收一个父 context 。下面简要介绍上面这几个函数。
WithCancel()
它返回一个 context ,并返回一个 CancelFunc
无参函数,如果调用该函数,其会往 context.Done()
这个方法的 chan 发送一个取消信号。让所有监听这个 chan 的都知道该 context 被取消了。上面使用 Context 控制协程的例子已经演示了这一点。
WithDeadline()
它同样会返回一个 CancelFunc
无参函数,但它参数带有一个时间戳(time.Time),即截止时间,当到达截止时间, context.Done()
这个方法的 chan 就会自动接收到一个完成的信号。
WithTimeout()
这个函数和上面的 WithDeadline()
差不多,但它带有一个具体的时间段(time.Duration)。
WithDeadline()
传入的第二个参数是 time.Time
类型,它是一个绝对的时间,意思是在什么时间点超时取消。
而 WithTimeout()
传入的第二个参数是 time.Duration
类型,它是一个相对的时间,意思是多长时间后超时取消。
WithValue()
使用该函数可以在原有的 context 中可以添加一些值,然后返回一个新的 context 。这些数据以 Key-Value 的方式传入,Key 必须有可比性,Value 必须是线程安全的。


Request Context
下面简单讲一讲请求上下文, Request 有一个方法:
func (r *Request) Context() context.Context {
if r.ctx != nil {
return r.ctx
}
return context.Background()
}
该方法返回当前请求的上下文。还有一个方法:
func(*Request) WithContext(ctx context.Context) context.Context
该方法基于当前的 Context 进行“修改”,实际上是创建一个新的 Context ,因为 Context 是不允许修改的。下面是一个使用 Context 处理请求超时的例子,这个例子基于上一期的中间件的例子,先在 middleware
目录下补充中间件 TimeoutMiddleware
:
package middleware
import (
"context"
"net/http"
"time"
)
type TimeoutMiddleware struct {
Next http.Handler
}
func (tm *TimeoutMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if tm.Next == nil {
tm.Next = http.DefaultServeMux
}
// 获取当前请求的上下文
ctx := r.Context()
// “修改” Context 设置 2s 超时
ctx, _ = context.WithTimeout(ctx, 2 * time.Second)
// 创建一个新的 Context 代替当前请求的 Context
r.WithContext(ctx)
// 接收信号的信道
ch := make(chan struct{})
go func() {
tm.Next.ServeHTTP(w, r)
// 执行完给信道发送执行完信号
ch <- struct{}{}
}()
select {
// 正常处理能得到执行完信号 返回
case <-ch:
return
// 从 ctx.Done() 得到信号证明超时
// 返回超时响应
case <-ctx.Done():
w.WriteHeader(http.StatusRequestTimeout)
}
ctx.Done()
}
修改 main.go 把 TimeoutMiddleware
中间件套在 AuthMiddleware
外面:
package main
import (
"encoding/json"
"goweb/middleware"
"net/http"
"time"
)
type Person struct {
Name string
Age int64
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
p := Person{
Name: "Caizi",
Age: 18,
}
enc := json.NewEncoder(w)
enc.Encode(p)
})
http.ListenAndServe("localhost:8080", &middleware.TimeoutMiddleware{
Next: new(middleware.AuthMiddleware),
})
}
同样,我们利用上次的 test.http 进行测试:
GET http://localhost:8080/ HTTP/1.1
Authorization: a
测试的结果是正常的,接下来再修改 main.go 增加休眠 3s 代码,使其响应超时:
package main
import (
"encoding/json"
"goweb/middleware"
"net/http"
"time"
)
type Person struct {
Name string
Age int64
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
p := Person{
Name: "Caizi",
Age: 18,
}
// 休眠 3s
time.Sleep(3 * time.Second)
enc := json.NewEncoder(w)
enc.Encode(p)
})
http.ListenAndServe("localhost:8080", &middleware.TimeoutMiddleware{
Next: new(middleware.AuthMiddleware),
})
}
测试后结果返回 408 Request Timeout
。