context的字面意思是上下文,是一個(gè)比較抽象的詞,字面上理解就是上下層的傳遞,上會(huì)把內(nèi)容傳遞給下,在go中程序單位一般為goroutine,這里的上下文便是在goroutine之間進(jìn)行傳遞。
根據(jù)現(xiàn)實(shí)例子來講,最??吹絚ontext的便是web端。一個(gè)網(wǎng)絡(luò)請求request請求服務(wù)端,每一個(gè)request都會(huì)開啟一個(gè)goroutine,這個(gè)goroutine在邏輯處理中可能會(huì)去開啟其他的goroutine,例如去開啟一個(gè)MongoDB的連接,一個(gè)request的goroutine開啟了很多個(gè)goroutine時(shí)候,需要對這些goroutine進(jìn)行控制,這時(shí)候就需要context來進(jìn)行對這些goroutine進(jìn)行跟蹤。即一個(gè)請求Request,會(huì)需要多個(gè)Goroutine中處理。而這些Goroutine可能需要共享Request的一些信息;同時(shí)當(dāng)Request被取消或者超時(shí)的時(shí)候,所有從這個(gè)Request創(chuàng)建的所有Goroutine也應(yīng)該被結(jié)束。
例子講述完畢,用go的風(fēng)格再講一次。
在每一個(gè)goroutine在執(zhí)行之前,都要知道程序當(dāng)前的執(zhí)行狀態(tài),這些狀態(tài)都被封裝在context變量中,要傳遞給要執(zhí)行的goroutine中去,這個(gè)上下文就成為了傳遞與請求同生存周期變量的標(biāo)準(zhǔn)方法。
注意 context是在go 1.7版本之后引入的,以前版本的注意(go更新特別快,每一個(gè)版本都變得越來越好,自己第一次接觸go語言的時(shí)候才1.9版本,實(shí)習(xí)公司用的好像是1.7,研發(fā)團(tuán)隊(duì)解體后現(xiàn)在實(shí)習(xí)用的版本是1.11 短時(shí)間版本就如此之大,1.10版本G-M模型改為G-P-M模型,聽聞1.12社區(qū)會(huì)再次優(yōu)化GC垃圾回收,引入分代)
Context接口
Context的接口定義的比較簡潔,我們看下這個(gè)接口的方法。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
這個(gè)接口共有4個(gè)方法,了解這些方法的意思非常重要,這樣我們才可以更好的使用他們。
Deadline方法是獲取設(shè)置的截止時(shí)間的意思,第一個(gè)返回式是截止時(shí)間,到了這個(gè)時(shí)間點(diǎn),Context會(huì)自動(dòng)發(fā)起取消請求;第二個(gè)返回值ok==false時(shí)表示沒有設(shè)置截止時(shí)間,如果需要取消的話,需要調(diào)用取消函數(shù)進(jìn)行取消。
Done方法返回一個(gè)只讀的chan,類型為struct{},我們在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經(jīng)發(fā)起了取消請求,我們通過Done方法收到這個(gè)信號(hào)后,就應(yīng)該做清理操作,然后退出goroutine,釋放資源。
Err方法返回取消的錯(cuò)誤原因,因?yàn)槭裁碈ontext被取消。
Value方法獲取該Context上綁定的值,是一個(gè)鍵值對,所以要通過一個(gè)Key才可以獲取對應(yīng)的值,這個(gè)值一般是線程安全的。
有了如上的根Context,那么是如何衍生更多的子Context的呢?這就要靠context包為我們提供的With系列的函數(shù)了。
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
這四個(gè)With函數(shù),接收的都有一個(gè)partent參數(shù),就是父Context,我們要基于這個(gè)父Context創(chuàng)建出子Context的意思,這種方式可以理解為子Context對父Context的繼承,也可以理解為基于父Context的衍生。
通過這些函數(shù),就創(chuàng)建了一顆Context樹,樹的每個(gè)節(jié)點(diǎn)都可以有任意多個(gè)子節(jié)點(diǎn),節(jié)點(diǎn)層級可以有任意多個(gè)。
WithCancel函數(shù),傳遞一個(gè)父Context作為參數(shù),返回子Context,以及一個(gè)取消函數(shù)用來取消Context。 WithDeadline函數(shù),和WithCancel差不多,它會(huì)多傳遞一個(gè)截止時(shí)間參數(shù),意味著到了這個(gè)時(shí)間點(diǎn),會(huì)自動(dòng)取消Context,當(dāng)然我們也可以不等到這個(gè)時(shí)候,可以提前通過取消函數(shù)進(jìn)行取消。
WithTimeout和WithDeadline基本上一樣,這個(gè)表示是超時(shí)自動(dòng)取消,是多少時(shí)間后自動(dòng)取消Context的意思。
WithValue函數(shù)和取消Context無關(guān),它是為了生成一個(gè)綁定了一個(gè)鍵值對數(shù)據(jù)的Context,這個(gè)綁定的數(shù)據(jù)可以通過Context.Value方法訪問到
引用飛雪無情的代碼:
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("監(jiān)控退出,停止了...")
return
default:
fmt.Println("goroutine監(jiān)控中...")
time.Sleep(2 * time.Second)
}
}
}(ctx)
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知監(jiān)控停止")
cancel()
//為了檢測監(jiān)控過是否停止,如果沒有監(jiān)控輸出,就表示停止了
time.Sleep(5 * time.Second)
}
context.Background()返回一個(gè)空的Context,這個(gè)空的Context一般用于整個(gè)Context樹的根節(jié)點(diǎn)。然后我們使用context.WithCancel(parent)函數(shù),創(chuàng)建一個(gè)可取消的子Context,然后當(dāng)作參數(shù)傳給goroutine使用,這樣就可以使用這個(gè)子Context跟蹤這個(gè)goroutine。在goroutine中,使用select調(diào)用
<-ctx.Done()判斷是否要結(jié)束,如果接受到值的話,就可以返回結(jié)束goroutine了;如果接收不到,就會(huì)繼續(xù)進(jìn)行監(jiān)控。那么是如何發(fā)送結(jié)束指令的呢?這就是示例中的
cancel函數(shù)啦,它是我們調(diào)用context.WithCancel(parent)函數(shù)生成子Context的時(shí)候返回的,第二個(gè)返回值就是這個(gè)取消函數(shù),它是CancelFunc類型的。我們調(diào)用它就可以發(fā)出取消指令,然后我們的監(jiān)控goroutine就會(huì)收到信號(hào),就會(huì)返回結(jié)束。
在引用一段多控制
func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx,"【監(jiān)控1】")
go watch(ctx,"【監(jiān)控2】")
go watch(ctx,"【監(jiān)控3】")
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知監(jiān)控停止")
cancel()
//為了檢測監(jiān)控過是否停止,如果沒有監(jiān)控輸出,就表示停止了
time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name,"監(jiān)控退出,停止了...")
return
default:
fmt.Println(name,"goroutine監(jiān)控中...")
time.Sleep(2 * time.Second)
}
}
}
示例中啟動(dòng)了3個(gè)監(jiān)控goroutine進(jìn)行不斷的監(jiān)控,每一個(gè)都使用了Context進(jìn)行跟蹤,當(dāng)我們使用cancel函數(shù)通知取消時(shí),這3個(gè)goroutine都會(huì)被結(jié)束。這就是Context的控制能力,它就像一個(gè)控制器一樣,按下開關(guān)后,所有基于這個(gè)Context或者衍生的子Context都會(huì)收到通知,這時(shí)就可以進(jìn)行清理操作了,最終釋放goroutine,這就優(yōu)雅的解決了goroutine啟動(dòng)后不可控的問題。
在引用一次潘少大佬的代碼:
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()
//啟動(dòng)子goroutine是為了不阻塞當(dāng)前goroutine,這里在實(shí)際場景中可以去執(zhí)行其他邏輯,這里為了方便直接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里一般會(huì)處理一些IO任務(wù),如讀寫數(shù)據(jù)庫或者rpc調(diào)用,這里為了方便直接把數(shù)據(jù)打印
fmt.Println("printing ", ctx.Value(favContextKey("resp")))
time.Sleep(time.Second * 1)
}
}
}
首先調(diào)用context.Background()生成根節(jié)點(diǎn),然后調(diào)用withCancel方法,傳入根節(jié)點(diǎn),得到新的子Context以及根節(jié)點(diǎn)的cancel方法(通知所有子節(jié)點(diǎn)結(jié)束運(yùn)行),這里要注意:該方法也返回了一個(gè)Context,這是一個(gè)新的子節(jié)點(diǎn),與初始傳入的根節(jié)點(diǎn)不是同一個(gè)實(shí)例了,但是每一個(gè)子節(jié)點(diǎn)里會(huì)保存從最初的根節(jié)點(diǎn)到本節(jié)點(diǎn)的鏈路信息 ,才能實(shí)現(xiàn)鏈?zhǔn)健?/p>
程序的reqURL方法接收一個(gè)url,然后通過http請求該url獲得response,然后在當(dāng)前goroutine里再啟動(dòng)一個(gè)子groutine把response打印出來,然后從ReqURL開始Context樹往下衍生葉子節(jié)點(diǎn)(每一個(gè)鏈?zhǔn)秸{(diào)用新產(chǎn)生的ctx),中間每個(gè)ctx都可以通過WithValue方式傳值(實(shí)現(xiàn)通信),而每一個(gè)子goroutine都能通過Value方法從父goroutine取值,實(shí)現(xiàn)協(xié)程間的通信,每個(gè)子ctx可以調(diào)用Done方法檢測是否有父節(jié)點(diǎn)調(diào)用cancel方法通知子節(jié)點(diǎn)退出運(yùn)行,根節(jié)點(diǎn)的cancel調(diào)用會(huì)沿著鏈路通知到每一個(gè)子節(jié)點(diǎn),因此實(shí)現(xiàn)了強(qiáng)并發(fā)控制,流程如圖:

context使用規(guī)范
最后,Context雖然是神器,但開發(fā)者使用也要遵循基本法,以下是一些Context使用的規(guī)范:
- 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存在一個(gè)結(jié)構(gòu)體當(dāng)中,顯式地傳入函數(shù)。Context變量需要作為第一個(gè)參數(shù)使用,一般命名為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;即使方法允許,也不要傳入一個(gè)nil的Context,如果你不確定你要用什么Context的時(shí)候傳一個(gè)context.TODO;
- Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;使用context的Value相關(guān)方法只應(yīng)該用于在程序和接口中傳遞的和請求相關(guān)的元數(shù)據(jù),不要用它來傳遞一些可選的參數(shù);
- The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;同樣的Context可以用來傳遞到不同的goroutine中,Context在多個(gè)goroutine中是安全的;