Go標準庫Context

Go標準庫Context

在 Go http包的Server中构挤,每一個請求在都有一個對應的 goroutine 去處理焊切。請求處理函數(shù)通常會啟動額外的 goroutine 用來訪問后端服務,比如數(shù)據(jù)庫和RPC服務。用來處理一個請求的 goroutine 通常需要訪問一些與請求特定的數(shù)據(jù),比如終端用戶的身份認證信息、驗證相關的token椭岩、請求的截止時間。 當一個請求被取消或超時時璃赡,所有用來處理該請求的 goroutine 都應該迅速退出判哥,然后系統(tǒng)才能釋放這些 goroutine 占用的資源。

為什么需要Context

基本示例

package main

import (
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup

// 初始的例子

func worker() {
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
    }
    // 如何接收外部命令實現(xiàn)退出
    wg.Done()
}

func main() {
    wg.Add(1)
    go worker()
    // 如何優(yōu)雅的實現(xiàn)結束子goroutine
    wg.Wait()
    fmt.Println("over")
}

全局變量方式

package main

import (
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup
var exit bool

// 全局變量方式存在的問題:
// 1. 使用全局變量在跨包調(diào)用時不容易統(tǒng)一
// 2. 如果worker中再啟動goroutine碉考,就不太好控制了塌计。

func worker() {
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        if exit {
            break
        }
    }
    wg.Done()
}

func main() {
    wg.Add(1)
    go worker()
    time.Sleep(time.Second * 3) // sleep3秒以免程序過快退出
    exit = true                 // 修改全局變量實現(xiàn)子goroutine的退出
    wg.Wait()
    fmt.Println("over")
}

通道方式

package main

import (
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup

// 管道方式存在的問題:
// 1. 使用全局變量在跨包調(diào)用時不容易實現(xiàn)規(guī)范和統(tǒng)一,需要維護一個共用的channel

func worker(exitChan chan struct{}) {
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-exitChan: // 等待接收上級通知
            break LOOP
        default:
        }
    }
    wg.Done()
}

func main() {
    var exitChan = make(chan struct{})
    wg.Add(1)
    go worker(exitChan)
    time.Sleep(time.Second * 3) // sleep3秒以免程序過快退出
    exitChan <- struct{}{}      // 給子goroutine發(fā)送退出信號
    close(exitChan)
    wg.Wait()
    fmt.Println("over")
}

官方版的方案

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done(): // 等待上級通知
            break LOOP
        default:
        }
    }
    wg.Done()
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 3)
    cancel() // 通知子goroutine結束
    wg.Wait()
    fmt.Println("over")
}

當子goroutine又開啟另外一個goroutine時侯谁,只需要將ctx傳入即可:

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
    go worker2(ctx)
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done(): // 等待上級通知
            break LOOP
        default:
        }
    }
    wg.Done()
}

func worker2(ctx context.Context) {
LOOP:
    for {
        fmt.Println("worker2")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done(): // 等待上級通知
            break LOOP
        default:
        }
    }
}
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 3)
    cancel() // 通知子goroutine結束
    wg.Wait()
    fmt.Println("over")
}

Context初識

Go1.7加入了一個新的標準庫context锌仅,它定義了Context類型,專門用來簡化 對于處理單個請求的多個 goroutine 之間與請求域的數(shù)據(jù)墙贱、取消信號热芹、截止時間等相關操作,這些操作可能涉及多個 API 調(diào)用惨撇。

對服務器傳入的請求應該創(chuàng)建上下文伊脓,而對服務器的傳出調(diào)用應該接受上下文。它們之間的函數(shù)調(diào)用鏈必須傳遞上下文魁衙,或者可以使用WithCancel报腔、WithDeadlineWithTimeoutWithValue創(chuàng)建的派生上下文剖淀。當一個上下文被取消時榄笙,它派生的所有上下文也被取消。

Context接口

context.Context是一個接口祷蝌,該接口定義了四個需要實現(xiàn)的方法茅撞。具體簽名如下:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

其中:

  • Deadline方法需要返回當前Context被取消的時間,也就是完成工作的截止時間(deadline);
  • Done方法需要返回一個Channel米丘,這個Channel會在當前工作完成或者上下文被取消之后關閉剑令,多次調(diào)用Done方法會返回同一個Channel;
  • Err方法會返回當前Context結束的原因拄查,它只會在Done返回的Channel被關閉時才會返回非空的值吁津;
    • 如果當前Context被取消就會返回Canceled錯誤;
    • 如果當前Context超時就會返回DeadlineExceeded錯誤堕扶;
  • Value方法會從Context中返回鍵對應的值碍脏,對于同一個上下文來說,多次調(diào)用Value 并傳入相同的Key會返回相同的結果稍算,該方法僅用于傳遞跨API和進程間跟請求域的數(shù)據(jù)典尾;

Background()和TODO()

Go內(nèi)置兩個函數(shù):Background()TODO(),這兩個函數(shù)分別返回一個實現(xiàn)了Context接口的backgroundtodo糊探。我們代碼中最開始都是以這兩個內(nèi)置的上下文對象作為最頂層的partent context钾埂,衍生出更多的子上下文對象。

Background()主要用于main函數(shù)科平、初始化以及測試代碼中褥紫,作為Context這個樹結構的最頂層的Context,也就是根Context瞪慧。

TODO()髓考,它目前還不知道具體的使用場景,如果我們不知道該使用什么Context的時候弃酌,可以使用這個绳军。

backgroundtodo本質(zhì)上都是emptyCtx結構體類型,是一個不可取消矢腻,沒有設置截止時間门驾,沒有攜帶任何值的Context

With系列函數(shù)

此外多柑,context包中還定義了四個With系列函數(shù)奶是。

WithCancel

WithCancel的函數(shù)簽名如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel返回帶有新Done通道的父節(jié)點的副本。當調(diào)用返回的cancel函數(shù)或當關閉父上下文的Done通道時竣灌,將關閉返回上下文的Done通道聂沙,無論先發(fā)生什么情況。

取消此上下文將釋放與其關聯(lián)的資源初嘹,因此代碼應該在此上下文中運行的操作完成后立即調(diào)用cancel及汉。

func gen(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // return結束該goroutine,防止泄露
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 當我們?nèi)⊥晷枰恼麛?shù)后調(diào)用cancel

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
}

上面的示例代碼中屯烦,gen函數(shù)在單獨的goroutine中生成整數(shù)并將它們發(fā)送到返回的通道坷随。 gen的調(diào)用者在使用生成的整數(shù)之后需要取消上下文房铭,以免gen啟動的內(nèi)部goroutine發(fā)生泄漏。

WithDeadline

WithDeadline的函數(shù)簽名如下:

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

返回父上下文的副本温眉,并將deadline調(diào)整為不遲于d缸匪。如果父上下文的deadline已經(jīng)早于d,則WithDeadline(parent, d)在語義上等同于父上下文类溢。當截止日過期時凌蔬,當調(diào)用返回的cancel函數(shù)時,或者當父上下文的Done通道關閉時闯冷,返回上下文的Done通道將被關閉砂心,以最先發(fā)生的情況為準。

取消此上下文將釋放與其關聯(lián)的資源蛇耀,因此代碼應該在此上下文中運行的操作完成后立即調(diào)用cancel辩诞。

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // 盡管ctx會過期,但在任何情況下調(diào)用它的cancel函數(shù)都是很好的實踐蒂窒。
    // 如果不這樣做躁倒,可能會使上下文及其父類存活的時間超過必要的時間荞怒。
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}

上面的代碼中洒琢,定義了一個50毫秒之后過期的deadline,然后我們調(diào)用context.WithDeadline(context.Background(), d)得到一個上下文(ctx)和一個取消函數(shù)(cancel)褐桌,然后使用一個select讓主程序陷入等待:等待1秒后打印overslept退出或者等待ctx過期后退出衰抑。 因為ctx50秒后就過期,所以ctx.Done()會先接收到值荧嵌,上面的代碼會打印ctx.Err()取消原因呛踊。

WithTimeout

WithTimeout的函數(shù)簽名如下:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout返回WithDeadline(parent, time.Now().Add(timeout))

取消此上下文將釋放與其相關的資源啦撮,因此代碼應該在此上下文中運行的操作完成后立即調(diào)用cancel谭网,通常用于數(shù)據(jù)庫或者網(wǎng)絡連接的超時控制。具體示例如下:

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

// context.WithTimeout

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
    for {
        fmt.Println("db connecting ...")
        time.Sleep(time.Millisecond * 10) // 假設正常連接數(shù)據(jù)庫耗時10毫秒
        select {
        case <-ctx.Done(): // 50毫秒后自動調(diào)用
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

func main() {
    // 設置一個50毫秒的超時
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 5)
    cancel() // 通知子goroutine結束
    wg.Wait()
    fmt.Println("over")
}

WithValue

WithValue函數(shù)能夠?qū)⒄埱笞饔糜虻臄?shù)據(jù)與 Context 對象建立關系赃春。聲明如下:

func WithValue(parent Context, key, val interface{}) Context

WithValue返回父節(jié)點的副本愉择,其中與key關聯(lián)的值為val

僅對API和進程間傳遞請求域的數(shù)據(jù)使用上下文值织中,而不是使用它來傳遞可選參數(shù)給函數(shù)锥涕。

所提供的鍵必須是可比較的,并且不應該是string類型或任何其他內(nèi)置類型狭吼,以避免使用上下文在包之間發(fā)生沖突层坠。WithValue的用戶應該為鍵定義自己的類型。為了避免在分配給interface{}時進行分配刁笙,上下文鍵通常具有具體類型struct{}破花∏ぃ或者,導出的上下文關鍵變量的靜態(tài)類型應該是指針或接口旧乞。

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

// context.WithValue

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
    key := TraceCode("TRACE_CODE")
    traceCode, ok := ctx.Value(key).(string) // 在子goroutine中獲取trace code
    if !ok {
        fmt.Println("invalid trace code")
    }
LOOP:
    for {
        fmt.Printf("worker, trace code:%s\n", traceCode)
        time.Sleep(time.Millisecond * 10) // 假設正常連接數(shù)據(jù)庫耗時10毫秒
        select {
        case <-ctx.Done(): // 50毫秒后自動調(diào)用
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

func main() {
    // 設置一個50毫秒的超時
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    // 在系統(tǒng)的入口中設置trace code傳遞給后續(xù)啟動的goroutine實現(xiàn)日志數(shù)據(jù)聚合
    ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 5)
    cancel() // 通知子goroutine結束
    wg.Wait()
    fmt.Println("over")
}

使用Context的注意事項

  • 推薦以參數(shù)的方式顯示傳遞Context
  • 以Context作為參數(shù)的函數(shù)方法蔚润,應該把Context作為第一個參數(shù)。
  • 給一個函數(shù)方法傳遞Context的時候尺栖,不要傳遞nil嫡纠,如果不知道傳遞什么,就使用context.TODO()
  • Context的Value相關方法應該傳遞請求域的必要數(shù)據(jù)延赌,不應該用于傳遞可選參數(shù)
  • Context是線程安全的除盏,可以放心的在多個goroutine中傳遞

客戶端超時取消示例

調(diào)用服務端API時如何在客戶端實現(xiàn)超時控制?

server端

// context_timeout/server/main.go
package main

import (
    "fmt"
    "math/rand"
    "net/http"

    "time"
)

// server端挫以,隨機出現(xiàn)慢響應

func indexHandler(w http.ResponseWriter, r *http.Request) {
    number := rand.Intn(2)
    if number == 0 {
        time.Sleep(time.Second * 10) // 耗時10秒的慢響應
        fmt.Fprintf(w, "slow response")
        return
    }
    fmt.Fprint(w, "quick response")
}

func main() {
    http.HandleFunc("/", indexHandler)
    err := http.ListenAndServe(":8000", nil)
    if err != nil {
        panic(err)
    }
}

client端

// context_timeout/client/main.go
package main

import (
    "context"
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
    "time"
)

// 客戶端

type respData struct {
    resp *http.Response
    err  error
}

func doCall(ctx context.Context) {
    transport := http.Transport{
       // 請求頻繁可定義全局的client對象并啟用長鏈接
       // 請求不頻繁使用短鏈接
       DisableKeepAlives: true,     }
    client := http.Client{
        Transport: &transport,
    }

    respChan := make(chan *respData, 1)
    req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
    if err != nil {
        fmt.Printf("new requestg failed, err:%v\n", err)
        return
    }
    req = req.WithContext(ctx) // 使用帶超時的ctx創(chuàng)建一個新的client request
    var wg sync.WaitGroup
    wg.Add(1)
    defer wg.Wait()
    go func() {
        resp, err := client.Do(req)
        fmt.Printf("client.do resp:%v, err:%v\n", resp, err)
        rd := &respData{
            resp: resp,
            err:  err,
        }
        respChan <- rd
        wg.Done()
    }()

    select {
    case <-ctx.Done():
        //transport.CancelRequest(req)
        fmt.Println("call api timeout")
    case result := <-respChan:
        fmt.Println("call server api success")
        if result.err != nil {
            fmt.Printf("call server api failed, err:%v\n", result.err)
            return
        }
        defer result.resp.Body.Close()
        data, _ := ioutil.ReadAll(result.resp.Body)
        fmt.Printf("resp:%v\n", string(data))
    }
}

func main() {
    // 定義一個100毫秒的超時
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
    defer cancel() // 調(diào)用cancel釋放子goroutine資源
    doCall(ctx)
}
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末者蠕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子掐松,更是在濱河造成了極大的恐慌踱侣,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件大磺,死亡現(xiàn)場離奇詭異抡句,居然都是意外死亡,警方通過查閱死者的電腦和手機杠愧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門待榔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人流济,你說我怎么就攤上這事锐锣。” “怎么了绳瘟?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵雕憔,是天一觀的道長。 經(jīng)常有香客問我糖声,道長斤彼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任姨丈,我火速辦了婚禮畅卓,結果婚禮上,老公的妹妹穿的比我還像新娘蟋恬。我一直安慰自己翁潘,他們只是感情好,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布歼争。 她就那樣靜靜地躺著拜马,像睡著了一般渗勘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俩莽,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天旺坠,我揣著相機與錄音,去河邊找鬼扮超。 笑死取刃,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的出刷。 我是一名探鬼主播璧疗,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼馁龟!你這毒婦竟也來了崩侠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤坷檩,失蹤者是張志新(化名)和其女友劉穎却音,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矢炼,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡系瓢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了裸删。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片八拱。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡阵赠,死狀恐怖涯塔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情清蚀,我是刑警寧澤匕荸,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站枷邪,受9級特大地震影響榛搔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜东揣,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一践惑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嘶卧,春花似錦尔觉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽专甩。三九已至,卻和暖如春钉稍,著一層夾襖步出監(jiān)牢的瞬間涤躲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工贡未, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留种樱,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓俊卤,卻偏偏與公主長得像缸托,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瘾蛋,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容

  • context context包定義了上下文類型俐镐,該類型在API邊界之間以及進程之間傳遞截止日期,取消信號和其他請...
    DevilRoshan閱讀 379評論 0 0
  • 這篇文章將:介紹context工作機制哺哼;簡單說明接口和結構體功能佩抹;通過簡單Demo介紹外部API創(chuàng)建并使用cont...
    Ovenvan考研停更閱讀 988評論 0 12
  • context.Context類型 context.Context類型(以下簡稱Context類型)是在Go 1....
    尼桑麻閱讀 6,149評論 0 4
  • Context 通常被譯作上下文,一般理解為程序單元的一個運行狀態(tài)取董、現(xiàn)場棍苹、快照,而翻譯中上下文又很好地詮釋了它的本...
    Asphalt7閱讀 559評論 0 0
  • 本文從上下文Context茵汰、同步原語與鎖枢里、Channel、調(diào)度器四個方面介紹Go語言是如何實現(xiàn)并發(fā)的蹂午。本文絕大部分...
    彥幀閱讀 1,571評論 1 3