Go 语言中的 Context_json

Go 语言中的 Context_json_02

什么是 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 才可以获取对应的值,这个值一般是线程安全的。

上面这些方法都是用于读取的,不能进行设置。


Go 语言中的 Context_json

Go 语言中的 Context_json_02

使用 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 接收值{} 监控结束
主程序退出

可以看到和上面使用信道控制协程的效果相同。


Go 语言中的 Context_json

Go 语言中的 Context_json_02

根 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
}

Go 语言中的 Context_json

Go 语言中的 Context_json_02

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 必须是线程安全的。


Go 语言中的 Context_json

Go 语言中的 Context_json_02

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​​ 。