context的字面意思是上下文站楚,是一個(gè)比較抽象的詞,字面上理解就是上下層的傳遞搏嗡,上會(huì)把內(nèi)容傳遞給下窿春,在go中程序單位一般為goroutine,這里的上下文便是在goroutine之間進(jìn)行傳遞采盒。
根據(jù)現(xiàn)實(shí)例子來講旧乞,最常看到context的便是web端磅氨。一個(gè)網(wǎng)絡(luò)請(qǐng)求request請(qǐng)求服務(wù)端尺栖,每一個(gè)request都會(huì)開啟一個(gè)goroutine,這個(gè)goroutine在邏輯處理中可能會(huì)去開啟其他的goroutine烦租,例如去開啟一個(gè)MongoDB的連接延赌,一個(gè)request的goroutine開啟了很多個(gè)goroutine時(shí)候除盏,需要對(duì)這些goroutine進(jìn)行控制,這時(shí)候就需要context來進(jìn)行對(duì)這些goroutine進(jìn)行跟蹤挫以。即一個(gè)請(qǐng)求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è)上下文就成為了傳遞與請(qǐng)求同生存周期變量的標(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ā)起取消請(qǐng)求;第二個(gè)返回值ok==false時(shí)表示沒有設(shè)置截止時(shí)間沐绒,如果需要取消的話俩莽,需要調(diào)用取消函數(shù)進(jìn)行取消。
Done
方法返回一個(gè)只讀的chan洒沦,類型為struct{}
豹绪,我們在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經(jīng)發(fā)起了取消請(qǐng)求瞒津,我們通過Done
方法收到這個(gè)信號(hào)后蝉衣,就應(yīng)該做清理操作,然后退出goroutine巷蚪,釋放資源病毡。
Err
方法返回取消的錯(cuò)誤原因,因?yàn)槭裁碈ontext被取消屁柏。
Value
方法獲取該Context上綁定的值啦膜,是一個(gè)鍵值對(duì),所以要通過一個(gè)Key才可以獲取對(duì)應(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對(duì)父Context的繼承匕荸,也可以理解為基于父Context的衍生爹谭。
通過這些函數(shù),就創(chuàng)建了一顆Context樹榛搔,樹的每個(gè)節(jié)點(diǎn)都可以有任意多個(gè)子節(jié)點(diǎn)诺凡,節(jié)點(diǎn)層級(jí)可以有任意多個(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è)鍵值對(duì)數(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請(qǐng)求該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)該用于在程序和接口中傳遞的和請(qǐ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中是安全的;