溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Go 語言上下文 Context的含義和用法

發(fā)布時間:2021-06-18 09:27:07 來源:億速云 閱讀:671 作者:chen 欄目:編程語言

這篇文章主要介紹“Go 語言上下文 Context的含義和用法”,在日常操作中,相信很多人在Go 語言上下文 Context的含義和用法問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Go 語言上下文 Context的含義和用法”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

上下文(Context)是 Go 語言中非常有特色的一個特性, 在 Go 1.7 版本中正式引入新標準庫 context。

其主要的作用是在 goroutine 中進行上下文的傳遞,而在傳遞信息中又包含了 goroutine 的運行控制、上下文信息傳遞等功能。

Go 語言上下文 Context的含義和用法

為加強大家對 Go 語言的 context 的設計,本文將對標準庫 context 進行深入剖析,看看他里面到底暗含了何物,又為何能夠做那么多事。

整體的描述結(jié)構(gòu)是:“了解 context 特性,熟悉 context 流程,剖析 context 原理” 三個板塊進行。目錄如下:

Go 語言上下文 Context的含義和用法

什么是 context

Go 語言的獨有的功能之一 Context,最常聽說開發(fā)者說的一句話就是 “函數(shù)的第一個形參真的要傳 ctx 嗎?”,第二句話可能是  “有沒有什么辦法不傳,就能達到傳入的效果?”,聽起來非常魔幻。

在 Go 語言中 context 作為一個 “一等公民” 的標準庫,許多的開源庫都一定會對他進行支持,因為標準庫 context  的定位是上下文控制。會在跨 goroutine 中進行傳播:

Go 語言上下文 Context的含義和用法

本質(zhì)上 Go 語言是基于 context 來實現(xiàn)和搭建了各類 goroutine 控制的,并且與  select-case聯(lián)合,就可以實現(xiàn)進行上下文的截止時間、信號控制、信息傳遞等跨 goroutine 的操作,是 Go 語言協(xié)程的重中之重。

context 基本特性

演示代碼:

func main() {  parentCtx := context.Background()  ctx, cancel := context.WithTimeout(parentCtx, 1*time.Millisecond)  defer cancel()   select {  case <-time.After(1 * time.Second):   fmt.Println("overslept")  case <-ctx.Done():   fmt.Println(ctx.Err())  } }

輸出結(jié)果:

context deadline exceeded

我們通過調(diào)用標準庫 context.WithTimeout 方法針對 parentCtx 變量設置了超時時間,并在隨后調(diào)用 select-case 進行  context.Done 方法的監(jiān)聽,最后由于達到截止時間。因此邏輯上 select 走到了 context.Err 的 case 分支,最終輸出  context deadline exceeded。

除了上述所描述的方法外,標準庫 context 還支持下述方法:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) type Context     func Background() Context     func TODO() Context     func WithValue(parent Context, key, val interface{}) Context
  • WithCancel:基于父級 context,創(chuàng)建一個可以取消的新 context。

  • WithDeadline:基于父級 context,創(chuàng)建一個具有截止時間(Deadline)的新 context。

  • WithTimeout:基于父級 context,創(chuàng)建一個具有超時時間(Timeout)的新 context。

  • Background:創(chuàng)建一個空的 context,一般常用于作為根的父級 context。

  • TODO:創(chuàng)建一個空的 context,一般用于未確定時的聲明使用。

  • WithValue:基于某個 context 創(chuàng)建并存儲對應的上下文信息。

context 本質(zhì)

我們在基本特性中介紹了不少 context 的方法,其基本大同小異??瓷先ニ坪醪浑y,接下來我們看看其底層的基本原理和設計。

context 相關(guān)函數(shù)的標準返回如下:

func WithXXXX(parent Context, xxx xxx) (Context, CancelFunc)

其返回值分別是 Context 和 CancelFunc,接下來我們將進行分析這兩者的作用。

接口

1. Context 接口:

type Context interface {     Deadline() (deadline time.Time, ok bool)     Done() <-chan struct{}     Err() error     Value(key interface{}) interface{} }
  • Deadline:獲取當前 context 的截止時間。

  • Done:獲取一個只讀的 channel,類型為結(jié)構(gòu)體??捎糜谧R別當前 channel 是否已經(jīng)被關(guān)閉,其原因可能是到期,也可能是被取消了。

  • Err:獲取當前 context 被關(guān)閉的原因。

  • Value:獲取當前 context 對應所存儲的上下文信息。

2. Canceler 接口:

type canceler interface {  cancel(removeFromParent bool, err error)  Done() <-chan struct{} }
  • cancel:調(diào)用當前 context 的取消方法。

  • Done:與前面一致,可用于識別當前 channel 是否已經(jīng)被關(guān)閉。

基礎(chǔ)結(jié)構(gòu)

在標準庫 context 的設計上,一共提供了四類 context 類型來實現(xiàn)上述接口。分別是 emptyCtx、cancelCtx、timerCtx  以及 valueCtx。

Go 語言上下文 Context的含義和用法

emptyCtx

在日常使用中,常常使用到的 context.Background 方法,又或是 context.TODO 方法。

源碼如下:

var (  background = new(emptyCtx)  todo       = new(emptyCtx) )  func Background() Context {  return background }  func TODO() Context {  return todo }

其本質(zhì)上都是基于 emptyCtx 類型的基本封裝。而 emptyCtx 類型本質(zhì)上是實現(xiàn)了 Context 接口:

type emptyCtx int  func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {  return }  func (*emptyCtx) Done() <-chan struct{} {  return nil }  func (*emptyCtx) Err() error {  return nil }  func (*emptyCtx) Value(key interface{}) interface{} {  return nil }

實際上 emptyCtx 類型的 context 的實現(xiàn)非常簡單,因為他是空 context 的定義,因此沒有 deadline,更沒有  timeout,可以認為就是一個基礎(chǔ)空白 context 模板。

cancelCtx

在調(diào)用 context.WithCancel 方法時,我們會涉及到 cancelCtx 類型,其主要特性是取消事件。源碼如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {  c := newCancelCtx(parent)  propagateCancel(parent, &c)  return &c, func() { c.cancel(true, Canceled) } }  func newCancelCtx(parent Context) cancelCtx {  return cancelCtx{Context: parent} }

其中的 newCancelCtx 方法將會生成出一個可以取消的新 context,如果該 context 執(zhí)行取消,與其相關(guān)聯(lián)的子 context  以及對應的 goroutine 也會收到取消信息。

首先 main goroutine 創(chuàng)建并傳遞了一個新的 context 給 goroutine b,此時 goroutine b 的 context 是  main goroutine context 的子集:

Go 語言上下文 Context的含義和用法

傳遞過程中,goroutine b 再將其 context 一個個傳遞給了 goroutine c、d、e。最后在運行時 goroutine b 調(diào)用了  cancel 方法。使得該 context 以及其對應的子集均接受到取消信號,對應的 goroutine 也進行了響應。

接下來我們針對 cancelCtx 類型來進一步看看:

type cancelCtx struct {  Context   mu       sync.Mutex            // protects following fields  done     chan struct{}         // created lazily, closed by first cancel call  children map[canceler]struct{} // set to nil by the first cancel call  err      error                 // set to non-nil by the first cancel call }

該結(jié)構(gòu)體所包含的屬性也比較簡單,主要是 children 字段,其包含了該 context 對應的所有子集  context,便于在后續(xù)發(fā)生取消事件的時候進行逐一通知和關(guān)聯(lián)。

而其他的屬性主要用于并發(fā)控制(互斥鎖)、取消信息和錯誤的寫入:

func (c *cancelCtx) Value(key interface{}) interface{} {  if key == &cancelCtxKey {   return c  }  return c.Context.Value(key) }  func (c *cancelCtx) Done() <-chan struct{} {  c.mu.Lock()  if c.done == nil {   c.done = make(chan struct{})  }  d := c.done  c.mu.Unlock()  return d }  func (c *cancelCtx) Err() error {  c.mu.Lock()  err := c.err  c.mu.Unlock()  return err }

在上述代碼中可以留意到,done 屬性(只讀 channel)是在真正調(diào)用到 Done 方法時才會去創(chuàng)建。需要配合 select-case  來使用。

timerCtx

在調(diào)用 context.WithTimeout 方法時,我們會涉及到 timerCtx 類型,其主要特性是 Timeout 和 Deadline  事件,源碼如下:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {  return WithDeadline(parent, time.Now().Add(timeout)) }  func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {  ...  c := &timerCtx{   cancelCtx: newCancelCtx(parent),   deadline:  d,  } }

你可以發(fā)現(xiàn) timerCtx 類型是基于 cancelCtx 類型的。我們再進一步看看 timerCtx 結(jié)構(gòu)體:

type timerCtx struct {  cancelCtx  timer *time.Timer // Under cancelCtx.mu.   deadline time.Time }

其實 timerCtx 類型也就是 cancelCtx 類型,加上 time.Timer 和對應的 Deadline,也就是包含了時間屬性的控制。

我們進一步看看其配套的 cancel 方法,思考一下其是如何進行取消動作的:

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {  return c.deadline, true }  func (c *timerCtx) cancel(removeFromParent bool, err error) {  c.cancelCtx.cancel(false, err)  if removeFromParent {   removeChild(c.cancelCtx.Context, c)  }  c.mu.Lock()  if c.timer != nil {   c.timer.Stop()   c.timer = nil  }  c.mu.Unlock() }

先會調(diào)用 cancelCtx 類型的取消事件。若存在父級節(jié)點,則移除當前 context 子節(jié)點,最后停止定時器并進行定時器重置。而 Deadline 或  Timeout 的行為則由 timerCtx 的 WithDeadline 方法實現(xiàn):

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {  if cur, ok := parent.Deadline(); ok && cur.Before(d) {   // The current deadline is already sooner than the new one.   return WithCancel(parent)  }  ... }

該方法會先進行前置判斷,若父級節(jié)點的 Deadline 時間早于當前所指定的 Deadline 時間,將會直接生成一個 cancelCtx 的  context。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {  ...  c := &timerCtx{   cancelCtx: newCancelCtx(parent),   deadline:  d,  }  propagateCancel(parent, c)  dur := time.Until(d)  if dur <= 0 {   c.cancel(true, DeadlineExceeded) // deadline has already passed   return c, func() { c.cancel(false, Canceled) }  }  c.mu.Lock()  defer c.mu.Unlock()  if c.err == nil {   c.timer = time.AfterFunc(dur, func() {    c.cancel(true, DeadlineExceeded)   })  }  return c, func() { c.cancel(true, Canceled) } }

接下來將會正式生成成為一個 timeCtx 類型,并將其加入到父級 context 是 children 屬性中。最后進行當前時間與 Deadline  時間的計算,并通過調(diào)用 time.AfterFunc 在到期后自動調(diào)用 cancel 方法發(fā)起取消事件,自然也就會觸發(fā)父子級的事件傳播。

valueCtx

在調(diào)用 context.WithValue 方法時,我們會涉及到 valueCtx 類型,其主要特性是涉及上下文信息傳遞,源碼如下:

func WithValue(parent Context, key, val interface{}) Context {  ...  if !reflectlite.TypeOf(key).Comparable() {   panic("key is not comparable")  }  return &valueCtx{parent, key, val} }

你會發(fā)現(xiàn) valueCtx 結(jié)構(gòu)體也非常的簡單,核心就是鍵值對:

type valueCtx struct {  Context  key, val interface{} }

其在配套方法上也不會太復雜,基本就是要求可比較,接著就是存儲匹配:

func (c *valueCtx) Value(key interface{}) interface{} {  if c.key == key {   return c.val  }  return c.Context.Value(key) }

這時候你可能又有疑問了,那多個父子級 context 是如何實現(xiàn)跨 context 的上下文信息獲取的?

這秘密其實在上面的 valueCtx 和 Value 方法中有所表現(xiàn):

Go 語言上下文 Context的含義和用法

本質(zhì)上 valueCtx 類型是一個單向鏈表,會在調(diào)用 Value  方法時先查詢自己的節(jié)點是否有該值。若無,則會通過自身存儲的上層父級節(jié)點的信息一層層向上尋找對應的值,直到找到為止。

而在實際的工程應用中,你會發(fā)現(xiàn)各大框架,例如:gin、grpc  等。他都是有自己再實現(xiàn)一套上下文信息的傳輸?shù)亩畏庋b,本意也是為了更好的管理和觀察上下文信息。

context 取消事件

在我們針對 context 的各類延伸類型和源碼進行了分析后。我們進一步提出一個疑問點,context 是如何實現(xiàn)跨 goroutine  的取消事件并傳播開來的,是如何實現(xiàn)的?

這個問題的答案就在于 WithCancel 和 WithDeadline 都會涉及到 propagateCancel  方法,其作用是構(gòu)建父子級的上下文的關(guān)聯(lián)關(guān)系,若出現(xiàn)取消事件時,就會進行處理:

func propagateCancel(parent Context, child canceler) {  done := parent.Done()  if done == nil {   return  }   select {  case <-done:   child.cancel(false, parent.Err())   return  default:  }  ... }
  • 當父級上下文(parent)的 Done 結(jié)果為 nil 時,將會直接返回,因為其不會具備取消事件的基本條件,可能該 context 是  Background、TODO 等方法產(chǎn)生的空白 context。

  • 當父級上下文(parent)的 Done 結(jié)果不為 nil 時,則發(fā)現(xiàn)父級上下文已經(jīng)被取消,作為其子級,該 context  將會觸發(fā)取消事件并返回父級上下文的取消原因。

func propagateCancel(parent Context, child canceler) {  ...  if p, ok := parentCancelCtx(parent); ok {   p.mu.Lock()   if p.err != nil {    child.cancel(false, p.err)   } else {    if p.children == nil {     p.children = make(map[canceler]struct{})    }    p.children[child] = struct{}{}   }   p.mu.Unlock()  } else {   atomic.AddInt32(&goroutines, +1)   go func() {    select {    case <-parent.Done():     child.cancel(false, parent.Err())    case <-child.Done():    }   }()  } }

經(jīng)過前面一個代碼片段的判斷,已得知父級 context 未觸發(fā)取消事件,當前父級和子級 context 均正常(未取消)。

將會執(zhí)行以下流程:

  • 調(diào)用 parentCancelCtx 方法找到具備取消功能的父級 context。并將當前 context,也就是 child 加入到 父級  context 的 children 列表中,等待后續(xù)父級 context 的取消事件通知和響應。

  • 調(diào)用 parentCancelCtx 方法沒有找到,將會啟動一個新的 goroutine 去監(jiān)聽父子 context 的取消事件通知。

通過對 context 的取消事件和整體源碼分析,可得知 cancelCtx 類型的上下文包含了其下屬的所有子節(jié)點信息:

Go 語言上下文 Context的含義和用法

也就是其在 children 屬性的 map[canceler]struct{}  存儲結(jié)構(gòu)上就已經(jīng)支持了子級關(guān)系的查找,也就自然可以進行取消事件傳播了。

而具體的取消事件的實際行為,則是在前面提到的 propagateCancel 方法中,會在執(zhí)行例如cacenl  方法時,會對父子級上下文分別進行狀態(tài)判斷,若滿足則進行取消事件,并傳播給子級同步取消。

總結(jié)

作為 Go 語言的核心功能之一,其實標準庫 context 非常的短小精悍,使用的都是基本的數(shù)據(jù)結(jié)構(gòu)和理念。既滿足了跨 goroutine  的調(diào)控控制,像是并發(fā)、超時控制等。

同時也滿足了上下文的信息傳遞。在工程應用中,例如像是鏈路ID、公共參數(shù)、鑒權(quán)校驗等,都會使用到 context 作為媒介。

目前官方對于 context  的建議是作為方法的首參數(shù)傳入,雖有些麻煩,但也有人選擇將其作為結(jié)構(gòu)體中的一個屬性傳入。但這也會帶來一些心智負擔,需要識別是否重新 new 一個。

也有人提出希望 Go2 取消掉 context,換成另外一種方法,但總體而言目前未見到正式的提案,這是我們都需要再思考的。

到此,關(guān)于“Go 語言上下文 Context的含義和用法”的學習就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI