Go中的HTTP請求處理概述

在剛剛過去的 2019 gopher china 大會上 context
概念被多次提起,包括很多框架的源碼也大量運(yùn)用了。看得出來 context 在
golang 的世界中是一個非常重要的知識點,所以有必要對 context
有一個基本的使用和認(rèn)知。官方文檔解釋和示例都比較詳細(xì)正規(guī),本著學(xué)習(xí)的態(tài)度翻譯一遍加深理解。

概覽

context 包定義了 Context 類型,它在 API
邊界和進(jìn)程之間傳遞截止時間,取消信號和其他請求作用域的值。

服務(wù)收到請求應(yīng)該要創(chuàng)建一個 Context,對服務(wù)的響應(yīng)應(yīng)該要接受一個
Context。它們之間的函數(shù)調(diào)用鏈必須傳遞 Context,也可以使用
WithCancel,WithDeadline,WithTimeout 或 WithValue 等方法創(chuàng)建派生
Context 替換它。取消 Context 后,也會取消從中派生的所有 Context。

WithCancel,WithDeadline 和 WithTimeout 函數(shù)接受
Context(父)并返回派生的 Context(子)和一個 CancelFunc 函數(shù)。調(diào)用
CancelFunc 函數(shù)會取消該派生的子 Context 及其孫子
Context,刪除父項對子項的引用,并停止任何關(guān)聯(lián)的計時器。如果沒有調(diào)用
CancelFunc 會泄漏子項和孫子項,直到父項被取消或計時器觸發(fā)。 go vet
工具檢查是否在所有控制流路徑上使用了 CancelFuncs。

使用 Contexts
的程序應(yīng)遵循這些規(guī)則,以保持各個包的接口一致,并啟用靜態(tài)分析工具來檢查上下文的傳遞:

不要將 Contexts 存儲在結(jié)構(gòu)類型中;相反,要將 Context
明確地傳遞給需要它的每個函數(shù)。 Context 應(yīng)該是第一個參數(shù),通常命名為
ctx:

func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}

即使函數(shù)允許,也不要傳遞 nil Context。如果你不確定要使用哪個
Context,請傳遞 context.TODO。

僅將上下文的值用于 API
邊界和進(jìn)程之間的請求作用域數(shù)據(jù),而不是將可選參數(shù)傳遞給函數(shù)。

可以將相同的 Context 傳遞給在不同 goroutine 中運(yùn)行的函數(shù);Contexts
對于同時使用多個 goroutine 是安全的。

有關(guān)服務(wù)中使用 Contexts
的示例代碼,請參考https://blog.golang.org/context

變量

Canceled 是上下文取消時,通過 Context.Err 返回的錯誤。

var Canceled = errors.New("context canceled")

DeadlineExceeded 是在上下文超過截止時間時,通過 Context.Err 返回的錯誤。

var DeadlineExceeded error = deadlineExceededError{}

函數(shù) WithCancel

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

WithCancel 返回帶有新 Done channel 的父副本。返回的上下文的 Done channel
在調(diào)用返回的取消函數(shù)或父上下文的 Done channel
關(guān)閉時關(guān)閉,取決于誰先發(fā)生。

取消此上下文會釋放與其相關(guān)的資源,因此代碼應(yīng)在此上下文中的操作完成后立即調(diào)用
cancel。

示例

此示例演示了使用可取消的上下文來防止 goroutine
泄漏。在示例函數(shù)的最后,gen 啟動的 goroutine 將返回,并且不會造成
goroutine 泄漏。

package main

import (
    "context"
    "fmt"
)

func main() {
    // gen 在單獨(dú)的 goroutine 中生成整數(shù)并將它們發(fā)送到返回的 channel。
    // 一旦消費(fèi)了生成的整數(shù),gen 的調(diào)用者需要取消上下文,從而不會泄漏 gen 啟動的內(nèi)部 goroutine。
    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // 返回以致不泄露 goroutine
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 當(dāng)我們消費(fèi)完整數(shù)后調(diào)用取消函數(shù)

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

Run in playground

函數(shù) WithDeadline

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

WithDeadline 返回父上下文的副本,其截止日期調(diào)整為不遲于
d。如果父級的截止日期早于 d,則 WithDeadline(parent, d)在語義上等同于
parent。返回的上下文的 Done channel
在超過截止時間后,調(diào)用返回的取消函數(shù)時或父上下文的 Done channel
關(guān)閉時關(guān)閉,三者取決于誰先發(fā)生。

取消此上下文會釋放與其關(guān)聯(lián)的資源,因此代碼應(yīng)在此上下文中的操作完成后立即調(diào)用
cancel。

示例

這個例子傳遞一個帶有任意截止時間的上下文來告訴一個阻塞的函數(shù)它應(yīng)該在超時的時候丟棄它的任務(wù)。

package main

import (
    "context"
    "fmt"
    "time"
)

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

    // 即使 ctx 將要過期,在任何情況下最好也要調(diào)用它的取消函數(shù)。
    // 如果不這樣做,可能會使上下文及其父級的活動時間超過必要時間。
    defer cancel()

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

}

Run in playground

函數(shù) WithTimeout

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

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

取消此上下文會釋放與其關(guān)聯(lián)的資源,因此代碼應(yīng)在此上下文中運(yùn)行的操作完成后立即調(diào)用
cancel:

func slowOperationWithTimeout(ctx context.Context) (Result, error) {
    ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()  // 如果 slowOperation 在超時之前完成,則釋放資源
completes before timeout elapses
    return slowOperation(ctx)
}
示例

此示例傳遞具有超時的上下文,以告知一個阻塞的函數(shù)在超時后它應(yīng)該丟棄它的任務(wù)。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // 傳遞一個帶超時的上下文,以告知一個阻塞的函數(shù)在超時后它應(yīng)該丟棄它的任務(wù)。
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) //打印 "context deadline exceeded"
    }

}

Run in playground

類型 CancelFunc

CancelFunc 通知一個操作丟棄它的任務(wù)。 CancelFunc
不等待任務(wù)停止。第一次調(diào)用后,CancelFunc 的后續(xù)調(diào)用將失效。

type CancelFunc func()

類型 Context

一個 Context 可以跨 API 邊界傳遞截止日期,取消信號和其他值。

Context 的方法可以由多個 goroutine 同時調(diào)用。

type Context interface {
        // Deadline 返回完成的任務(wù)的時間,即取消此上下文的時間。
        // 如果沒有設(shè)置截止時間,Deadline 返回 ok == false。
        // 對截止日期的連續(xù)調(diào)用返回相同的結(jié)果。
        Deadline() (deadline time.Time, ok bool)

        // 當(dāng)任務(wù)完成時,即此上下文被取消,Done 會返回一個關(guān)閉的channel。
        // 如果此上下文一直不被取消,Done 返回 nil。對 Done 的連續(xù)調(diào)用會返回相同的值。
        //
        // 當(dāng)取消函數(shù)被調(diào)用時,WithCancel 使 Done 關(guān)閉; 
        // 在截止時間到期時,WithDeadline 使 Done 關(guān)閉;
        // 當(dāng)超時的時候,WithTimeout使 Done 關(guān)閉。
        //
        // Done 可以使用 select 語句:
        //
        //  // Stream 使用 DoSomething 生成值并將它們發(fā)送到 out,
        //  // 直到 DoSomething 返回錯誤或 ctx.Done 關(guān)閉。
        //  func Stream(ctx context.Context, out chan<- Value) error {
        //      for {
        //          v, err := DoSomething(ctx)
        //          if err != nil {
        //              return err
        //          }
        //          select {
        //          case <-ctx.Done():
        //              return ctx.Err()
        //          case out <- v:
        //          }
        //      }
        //  }
        //
        // 查看 https://blog.golang.org/pipelines 獲得更多關(guān)于怎么使用 Done channel 去取消的例子
        Done() <-chan struct{}

        // 如果 Done 尚未關(guān)閉,則 Err 返回 nil。
        // 如果 Done 關(guān)閉,Err 會返回一個非nil的錯誤,原因:
        // 如果上下文被取消,則調(diào)用 Canceled;
        // 如果上下文的截止時間已過,則調(diào)用 DeadlineExceeded。
        // 在 Err 返回非 nil 錯誤后,對 Err 的連續(xù)調(diào)用返回相同的錯誤。
        Err() error

        // Value 返回與此上下文關(guān)聯(lián)的 key 的值,如果沒有值與 key 關(guān)聯(lián),則返回nil。使用相同的 key 連續(xù)調(diào)用 Value 會返回相同的結(jié)果。
        //
        // 僅將上下文的值用于API邊界和進(jìn)程之間的請求作用域數(shù)據(jù),而不是將可選參數(shù)傳遞給函數(shù)。
        //
        // key 標(biāo)識上下文中的特定值。
        // 在上下文中存儲值的函數(shù)通常在全局變量中分配一個 key,然后使用該 key 作為 context.WithValue 和 Context.Value 的參數(shù)。
        // key 可以是支持比較的任何類型
        // 包應(yīng)該將 key 定義為非導(dǎo)出類型以避免沖突。
        //
        // 定義 Context key 的包應(yīng)該為使用該 key 存儲的值提供類型安全的訪問:
        //
        //  // 包使用者定義一個存儲在上下文中的 User 類型。
        //  package user
        //
        //  import "context"
        //
        //  // User 是上下文中值的類型。
        //  type User struct {...}
        //
        //  // key 是此程序包中定義的 key 的非導(dǎo)出類型。
        //  // 這可以防止與其他包中定義的 key 沖突。
        //  type key int
        //
        //  // userKey 是上下文中 user.User 值的 key。它是不可以被導(dǎo)出的。
        //  // 客戶端使用 user.NewContext 和 user.FromContext 而不是直接使用 key。
        //  var userKey key
        //
        //  // NewContext 返回一個帶有值為 u 的新的上下文。
        //  func NewContext(ctx context.Context, u *User) context.Context {
        //      return context.WithValue(ctx, userKey, u)
        //  }
        //
        //  // FromContext 返回存儲在 ctx 中的 User 值(如果有的話)。
        //  func FromContext(ctx context.Context) (*User, bool) {
        //      u, ok := ctx.Value(userKey).(*User)
        //      return u, ok
        //  }
        Value(key interface{}) interface{}
}

函數(shù) Background

func Background() Context

Background 返回一個非 nil 的空
Context。它永遠(yuǎn)不會被取消,沒有值,也沒有截止時間。它通常由 main
函數(shù)初始化和測試使用,并作為請求的頂級 Context。

函數(shù) TODO

func TODO() Context

TODO 返回一個非 nil 的空 Context。當(dāng)不清楚使用哪個 Context
或者它還不可用時(因為周圍的函數(shù)尚未擴(kuò)展為接受 Context
參數(shù)),代碼應(yīng)該使用 context.TODO。

函數(shù) WithValue

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

WithValue 返回父級的副本,其中與 key 關(guān)聯(lián)的值為 val。

僅將上下文的值用于 API
邊界和進(jìn)程之間的請求作用域數(shù)據(jù),而不是將可選參數(shù)傳遞給函數(shù)。

提供的 key
必須是可比較的,不應(yīng)該是字符串類型或任何其他內(nèi)置類型,以避免使用上下文的包之間產(chǎn)生沖突。
WithValue 的使用者應(yīng)該為 keys 定義他們自己的自定義類型。為了避免在分配
interface{}時指定,上下文的 keys 通常具有具體類型 struct
{}。或者,導(dǎo)出的上下文的 key 變量的靜態(tài)類型應(yīng)該是指針或接口。

示例

此示例展示如何將值傳遞給上下文以及如何檢索它(如果存在)。

package main

import (
    "context"
    "fmt"
)

func main() {
    type favContextKey string

    f := func(ctx context.Context, k favContextKey) {
        if v := ctx.Value(k); v != nil {
            fmt.Println("found value:", v)
            return
        }
        fmt.Println("key not found:", k)
    }

    k := favContextKey("language")
    ctx := context.WithValue(context.Background(), k, "Go")

    f(ctx, k)
    f(ctx, favContextKey("color"))

}

Run in playground

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,978評論 2 374

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