context的字面意思是上下文,是一個比較抽象的詞,字面上理解就是上下層的傳遞,上會把內容傳遞給下,在go中程序單位一般為goroutine,這里的上下文便是在goroutine之間進行傳遞。
根據現實例子來講,最??吹絚ontext的便是web端。一個網絡請求request請求服務端,每一個request都會開啟一個goroutine,這個goroutine在邏輯處理中可能會去開啟其他的goroutine,例如去開啟一個MongoDB的連接,一個request的goroutine開啟了很多個goroutine時候,需要對這些goroutine進行控制,這時候就需要context來進行對這些goroutine進行跟蹤。即一個請求Request,會需要多個Goroutine中處理。而這些Goroutine可能需要共享Request的一些信息;同時當Request被取消或者超時的時候,所有從這個Request創建的所有Goroutine也應該被結束。
例子講述完畢,用go的風格再講一次。
在每一個goroutine在執行之前,都要知道程序當前的執行狀態,這些狀態都被封裝在context變量中,要傳遞給要執行的goroutine中去,這個上下文就成為了傳遞與請求同生存周期變量的標準方法。
注意 context是在go 1.7版本之后引入的,以前版本的注意(go更新特別快,每一個版本都變得越來越好,自己第一次接觸go語言的時候才1.9版本,實習公司用的好像是1.7,研發團隊解體后現在實習用的版本是1.11 短時間版本就如此之大,1.10版本G-M模型改為G-P-M模型,聽聞1.12社區會再次優化GC垃圾回收,引入分代)
Context接口
Context的接口定義的比較簡潔,我們看下這個接口的方法。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
這個接口共有4個方法,了解這些方法的意思非常重要,這樣我們才可以更好的使用他們。
Deadline
方法是獲取設置的截止時間的意思,第一個返回式是截止時間,到了這個時間點,Context會自動發起取消請求;第二個返回值ok==false時表示沒有設置截止時間,如果需要取消的話,需要調用取消函數進行取消。
Done
方法返回一個只讀的chan,類型為struct{}
,我們在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經發起了取消請求,我們通過Done
方法收到這個信號后,就應該做清理操作,然后退出goroutine,釋放資源。
Err
方法返回取消的錯誤原因,因為什么Context被取消。
Value
方法獲取該Context上綁定的值,是一個鍵值對,所以要通過一個Key才可以獲取對應的值,這個值一般是線程安全的。
有了如上的根Context,那么是如何衍生更多的子Context的呢?這就要靠context包為我們提供的With
系列的函數了。
Context的繼承衍生
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
這四個With
函數,接收的都有一個partent參數,就是父Context,我們要基于這個父Context創建出子Context的意思,這種方式可以理解為子Context對父Context的繼承,也可以理解為基于父Context的衍生。
通過這些函數,就創建了一顆Context樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個。
WithCancel
函數,傳遞一個父Context作為參數,返回子Context,以及一個取消函數用來取消Context。 WithDeadline
函數,和WithCancel
差不多,它會多傳遞一個截止時間參數,意味著到了這個時間點,會自動取消Context,當然我們也可以不等到這個時候,可以提前通過取消函數進行取消。
WithTimeout
和WithDeadline
基本上一樣,這個表示是超時自動取消,是多少時間后自動取消Context的意思。
WithValue
函數和取消Context無關,它是為了生成一個綁定了一個鍵值對數據的Context,這個綁定的數據可以通過Context.Value
方法訪問到
引用飛雪無情的代碼:
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("監控退出,停止了...")
return
default:
fmt.Println("goroutine監控中...")
time.Sleep(2 * time.Second)
}
}
}(ctx)
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知監控停止")
cancel()
//為了檢測監控過是否停止,如果沒有監控輸出,就表示停止了
time.Sleep(5 * time.Second)
}
context.Background()
返回一個空的Context,這個空的Context一般用于整個Context樹的根節點。然后我們使用context.WithCancel(parent)
函數,創建一個可取消的子Context,然后當作參數傳給goroutine使用,這樣就可以使用這個子Context跟蹤這個goroutine。在goroutine中,使用select調用
<-ctx.Done()
判斷是否要結束,如果接受到值的話,就可以返回結束goroutine了;如果接收不到,就會繼續進行監控。那么是如何發送結束指令的呢?這就是示例中的
cancel
函數啦,它是我們調用context.WithCancel(parent)
函數生成子Context的時候返回的,第二個返回值就是這個取消函數,它是CancelFunc
類型的。我們調用它就可以發出取消指令,然后我們的監控goroutine就會收到信號,就會返回結束。
在引用一段多控制
func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx,"【監控1】")
go watch(ctx,"【監控2】")
go watch(ctx,"【監控3】")
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知監控停止")
cancel()
//為了檢測監控過是否停止,如果沒有監控輸出,就表示停止了
time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name,"監控退出,停止了...")
return
default:
fmt.Println(name,"goroutine監控中...")
time.Sleep(2 * time.Second)
}
}
}
示例中啟動了3個監控goroutine進行不斷的監控,每一個都使用了Context進行跟蹤,當我們使用cancel
函數通知取消時,這3個goroutine都會被結束。這就是Context的控制能力,它就像一個控制器一樣,按下開關后,所有基于這個Context或者衍生的子Context都會收到通知,這時就可以進行清理操作了,最終釋放goroutine,這就優雅的解決了goroutine啟動后不可控的問題。
在引用一次潘少大佬的代碼:
package main
import (
"context"
"crypto/md5"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
type favContextKey string
func main() {
wg := &sync.WaitGroup{}
values := []string{"https://www.baidu.com/", "https://www.zhihu.com/"}
ctx, cancel := context.WithCancel(context.Background())
for _, url := range values {
wg.Add(1)
subCtx := context.WithValue(ctx, favContextKey("url"), url)
go reqURL(subCtx, wg)
}
go func() {
time.Sleep(time.Second * 3)
cancel()
}()
wg.Wait()
fmt.Println("exit main goroutine")
}
func reqURL(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
url, _ := ctx.Value(favContextKey("url")).(string)
for {
select {
case <-ctx.Done():
fmt.Printf("stop getting url:%s\n", url)
return
default:
r, err := http.Get(url)
if r.StatusCode == http.StatusOK && err == nil {
body, _ := ioutil.ReadAll(r.Body)
subCtx := context.WithValue(ctx, favContextKey("resp"), fmt.Sprintf("%s%x", url, md5.Sum(body)))
wg.Add(1)
go showResp(subCtx, wg)
}
r.Body.Close()
//啟動子goroutine是為了不阻塞當前goroutine,這里在實際場景中可以去執行其他邏輯,這里為了方便直接sleep一秒
// doSometing()
time.Sleep(time.Second * 1)
}
}
}
func showResp(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println("stop showing resp")
return
default:
//子goroutine里一般會處理一些IO任務,如讀寫數據庫或者rpc調用,這里為了方便直接把數據打印
fmt.Println("printing ", ctx.Value(favContextKey("resp")))
time.Sleep(time.Second * 1)
}
}
}
首先調用context.Background()生成根節點,然后調用withCancel方法,傳入根節點,得到新的子Context以及根節點的cancel方法(通知所有子節點結束運行),這里要注意:該方法也返回了一個Context,這是一個新的子節點,與初始傳入的根節點不是同一個實例了,但是每一個子節點里會保存從最初的根節點到本節點的鏈路信息 ,才能實現鏈式。
程序的reqURL方法接收一個url,然后通過http請求該url獲得response,然后在當前goroutine里再啟動一個子groutine把response打印出來,然后從ReqURL開始Context樹往下衍生葉子節點(每一個鏈式調用新產生的ctx),中間每個ctx都可以通過WithValue方式傳值(實現通信),而每一個子goroutine都能通過Value方法從父goroutine取值,實現協程間的通信,每個子ctx可以調用Done方法檢測是否有父節點調用cancel方法通知子節點退出運行,根節點的cancel調用會沿著鏈路通知到每一個子節點,因此實現了強并發控制,流程如圖:
context使用規范
最后,Context雖然是神器,但開發者使用也要遵循基本法,以下是一些Context使用的規范:
- Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx;不要把Context存在一個結構體當中,顯式地傳入函數。Context變量需要作為第一個參數使用,一般命名為ctx;
- Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use;即使方法允許,也不要傳入一個nil的Context,如果你不確定你要用什么Context的時候傳一個context.TODO;
- Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;使用context的Value相關方法只應該用于在程序和接口中傳遞的和請求相關的元數據,不要用它來傳遞一些可選的參數;
- The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;同樣的Context可以用來傳遞到不同的goroutine中,Context在多個goroutine中是安全的;