![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iN2ETNyQDM3EjMhFDNzADOzYzXwUDOxETM1IzLcFTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
什麼是 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 個方法:
-
:傳回的第一個值是 context 的截止時間,到了這個時間點,Context 會自動觸發 Cancel 動作。傳回的第二個值是布爾值, Deadline()
表示設定了截止時間, true
表示沒有設定截止時間,如果沒有設定截止時間,就要手動調用 cancel 函數取消 Context 。false
-
:傳回一個隻讀的通道(隻有在被 cancel 後才會傳回),類型為 Done()
。在子協程裡讀這個 channel ,除非被關閉,否則讀不出任何東西,根據這一點,就可以做一些清理動作,退出 goroutine 。struct{}
-
:傳回 context 被 cancel 的原因。例如是被取消,還是逾時。Err()
-
:傳回被綁定到 Context 的值,是一個鍵值對,是以要通過一個 Key 才可以擷取對應的值,這個值一般是線程安全的。Value()
上面這些方法都是用于讀取的,不能進行設定。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iN2ETNyQDM3EjMhFDNzADOzYzXwUDOxETM1IzLcFTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
使用 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 接收值{} 監控結束
主程式退出
可以看到和上面使用信道控制協程的效果相同。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iN2ETNyQDM3EjMhFDNzADOzYzXwUDOxETM1IzLcFTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
根 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
}
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iN2ETNyQDM3EjMhFDNzADOzYzXwUDOxETM1IzLcFTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
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 必須是線程安全的。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iN2ETNyQDM3EjMhFDNzADOzYzXwUDOxETM1IzLcFTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
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
。