前言
做爲go的使用者,大家應該都多多少少的見過Context包。可是因爲我得懶惰都沒有跳轉進去好好看看,導致我對Context包理解並不深。寫博客是一個很好的方式提醒自己不要懶惰,而且還能當成自己的筆記,平時翻一翻加深下記憶。
Context使用場景
目前總結兩種使用場景:
1. 主動停止groutine
2. 傳遞數據
Context是什麼?
Context直譯爲上下文,我們來看一下Context包中對它的說明,在go1.7之後已經添加到了標準庫中,我們之間可以在/src/context中查看。
Context包中對Context的說明
英文水平不是太好就直譯了過來,最近有在學習英文,遇到了很多優秀的人,真的是比你優秀的人比你還努力。又扯遠了…
- 包context定義了Context類型,幷包含deadlines(結束時間),cancelation signals(取消信號),和其他的請求api範圍的值。
- 對服務器的請求應該創建一個Context,服務器發出的外向請求應該接受Context。鏈式的函數調用之間必須傳遞Context,隨意的更換它使用一個導出Context使用WithCancel、WithDeadline、WithTimeout、WithValue。當一個Context取消所有從它導出的Contexts都會被取消。
- WithCancel、WithDeadline、WithTimeout函數攜帶一個Context(父)並返回一個導出的Context(子)和一個CancelFunc(取消函數)。調用CancelFunc函數來取消子和他的孩子,移除父母的參考孩子,並停止任何關聯的定時器。調用CancelFunc失敗會泄露子和他的孩子直到父取消或者計時器超時。go的審查工具被用在所有的control-flow路徑下檢查CancelFuncs。
程序在使用Context應遵循如下規則,以保證接口的一致,並允許靜態分析工具檢查Context傳遞。
1. 不要將 Contexts 放入結構體,相反context應該作爲第一個參數傳入,命名爲ctx。
func DoSomething(ctx context.Context, arg Arg) error {
... use ctx ...
}
2. 即使函數允許也不要傳遞一個nil的Context。如果不確定使用哪種Conetex,傳遞context.TODO
3. 使用context的Value相關方法只應該用於在程序和接口中傳遞的和請求相關的數據,不要用它來傳遞一些可選的參數。
4. 相同的Context可以在不同的goroutines中傳遞,Contexts是線程安全的。
Context包的核心
// context 包裏的方法是線程安全的,可以被多個 goroutine 使用
type Context interface {
// 如果存在,Deadline 返回Context將要關閉的時間
Deadline() (deadline time.Time, ok bool)
// 當Context 被 canceled 或是 times out 的時候,Done 返回一個被 closed 的channel
Done() <-chan struct{}
// 在 Done 的 channel被closed 後, Err 代表被關閉的原因
Err() error
// 如果存在,Value 返回與 key 相關了的值,不存在返回 nil
Value(key interface{}) interface{}
}
Context包中的導出方法
- Background()
該函數返回空的Context,該Context一般由接收請求的第一個Goroutine創建,是與進入請求對應的Context根節點,它不能被取消、沒有值、也沒有過期時間。它常常作爲處理Request的頂層context存在。 - TODO()
該函數返回空的Context,可以使用context.TODO,當不清楚使用哪個Context,或者不確定哪個可使用。 - CancelFunc()
該函數通知放棄操作者的工作,並不會等待工作停止。第一次調用之後,在調用不會做任何操作。 - WithCancel(parent Context)
該函數返回一個 cancelCtx ,同時返回一個 CancelFunc,CancelFunc 是 context 包中定義的一個函數類型:type CancelFunc func()。調用這個 CancelFunc 時,關閉對應的c.done,也就是讓他的後代goroutine退出。 - WithDeadline(parent Context, deadline time.Time)
該函數返回的Context類型值同樣是parent的副本,但其過期時間由deadline和parent的過期時間共同決定。當parent的過期時間早於傳入的deadline時間時,返回的過期時間應與parent相同。父節點過期時,其所有的子孫節點必須同時關閉;反之,返回的父節點的過期時間則爲deadline。 - WithTimeout(parent Context, timeout time.Duration)
WithTimeout函數與WithDeadline類似,只不過它傳入的是從現在開始Context剩餘的生命時長。他們都同樣也都返回了所創建的子Context的控制權,一個CancelFunc類型的函數變量。 - WithValue(parent Context, key, val interface{})
返回parent的一個副本,調用該副本的Value(key)方法將得到val。這樣我們不光將根節點原有的值保留了,還在子孫節點中加入了新的值,注意若存在Key相同,則會被覆蓋。
示例1 主動停止gorutine
import (
"context"
"fmt"
"time"
)
func run(ctx context.Context,threadId int){
for {
select {
case <-ctx.Done():
fmt.Println("timeout")
default:
fmt.Println("thred runing ", threadId)
}
}
}
func timeout(cancel context.CancelFunc){
time.Sleep( 1 * time.Second)
//取消
cancel()
}
func main() {
//使用Backgroud()
ctx, cancel := context.WithCancel(context.Background())
go run(ctx,1)
go run(ctx,2)
go run(ctx,3)
go timeout(cancel)
time.Sleep(2 * time.Second)
}
執行以上程序,會看到不斷的在打印threadId直到timeout調用了cancel主動停止,後開始不斷打印timeout直至主程序退出。示例主要展示了WithCancel,Background的用法。
在上面的例子中我們自己手寫了一個timeout,context包中已經給我們提供好了,下面我們就改造下。
示例2 使用WithDeadline,WithTimeout
import (
"context"
"fmt"
"time"
)
func run(ctx context.Context,threadId int){
for {
select {
case <-ctx.Done():
fmt.Println("timeout")
default:
fmt.Println("thred runing ", threadId)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(),1 * time.Second)
defer cancel()
go run(ctx,1)
go run(ctx,2)
go run(ctx,3)
time.Sleep(2 * time.Second)
}
由於WithTimeout與WithDeadline功能類似就不在舉例了,運行以上示例與示例1輸出同樣的結果。
示例3 傳遞數據
import (
"context"
"fmt"
"time"
)
var key string = "Hello word!"
func run(ctx context.Context){
for {
select {
case <-ctx.Done():
fmt.Println("timeout")
default:
fmt.Println(ctx.Value(key))
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(),1 * time.Second)
value := context.WithValue(ctx,key,"This is my test")
defer cancel()
go run(value)
time.Sleep(2 * time.Second)
}