自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一文吃透 Go 語言解密之上下文 Context

開發(fā) 后端
上下文(Context)是 Go 語言中非常有特色的一個(gè)特性, 在 Go 1.7 版本中正式引入新標(biāo)準(zhǔn)庫 context。

[[378300]]

大家好,我是煎魚。

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

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

 

為加強(qiáng)大家對 Go 語言的 context 的設(shè)計(jì),本文將對標(biāo)準(zhǔn)庫 context 進(jìn)行深入剖析,看看他里面到底暗含了何物,又為何能夠做那么多事。

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

 

什么是 context

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

在 Go 語言中 context 作為一個(gè) “一等公民” 的標(biāo)準(zhǔn)庫,許多的開源庫都一定會對他進(jìn)行支持,因?yàn)闃?biāo)準(zhǔn)庫 context 的定位是上下文控制。會在跨 goroutine 中進(jìn)行傳播:

 

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

context 基本特性

演示代碼:

  1. func main() { 
  2.  parentCtx := context.Background() 
  3.  ctx, cancel := context.WithTimeout(parentCtx, 1*time.Millisecond) 
  4.  defer cancel() 
  5.  
  6.  select { 
  7.  case <-time.After(1 * time.Second): 
  8.   fmt.Println("overslept"
  9.  case <-ctx.Done(): 
  10.   fmt.Println(ctx.Err()) 
  11.  } 

輸出結(jié)果:

  1. context deadline exceeded 

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

除了上述所描述的方法外,標(biāo)準(zhǔn)庫 context 還支持下述方法:

  1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 
  2. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) 
  3. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 
  4. type Context 
  5.     func Background() Context 
  6.     func TODO() Context 
  7.     func WithValue(parent Context, key, val interface{}) Context 
  • WithCancel:基于父級 context,創(chuàng)建一個(gè)可以取消的新 context。
  • WithDeadline:基于父級 context,創(chuàng)建一個(gè)具有截止時(shí)間(Deadline)的新 context。
  • WithTimeout:基于父級 context,創(chuàng)建一個(gè)具有超時(shí)時(shí)間(Timeout)的新 context。
  • Background:創(chuàng)建一個(gè)空的 context,一般常用于作為根的父級 context。
  • TODO:創(chuàng)建一個(gè)空的 context,一般用于未確定時(shí)的聲明使用。
  • WithValue:基于某個(gè) context 創(chuàng)建并存儲對應(yīng)的上下文信息。

context 本質(zhì)

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

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

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

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

接口

1. Context 接口:

  1. type Context interface { 
  2.     Deadline() (deadline time.Time, ok bool) 
  3.     Done() <-chan struct{} 
  4.     Err() error 
  5.     Value(key interface{}) interface{} 
  • Deadline:獲取當(dāng)前 context 的截止時(shí)間。
  • Done:獲取一個(gè)只讀的 channel,類型為結(jié)構(gòu)體??捎糜谧R別當(dāng)前 channel 是否已經(jīng)被關(guān)閉,其原因可能是到期,也可能是被取消了。
  • Err:獲取當(dāng)前 context 被關(guān)閉的原因。
  • Value:獲取當(dāng)前 context 對應(yīng)所存儲的上下文信息。

2. Canceler 接口:

  1. type canceler interface { 
  2.  cancel(removeFromParent bool, err error) 
  3.  Done() <-chan struct{} 
  • cancel:調(diào)用當(dāng)前 context 的取消方法。
  • Done:與前面一致,可用于識別當(dāng)前 channel 是否已經(jīng)被關(guān)閉。

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

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

 

emptyCtx

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

源碼如下:

  1. var ( 
  2.  background = new(emptyCtx) 
  3.  todo       = new(emptyCtx) 
  4.  
  5. func Background() Context { 
  6.  return background 
  7.  
  8. func TODO() Context { 
  9.  return todo 

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

  1. type emptyCtx int 
  2.  
  3. func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { 
  4.  return 
  5.  
  6. func (*emptyCtx) Done() <-chan struct{} { 
  7.  return nil 
  8.  
  9. func (*emptyCtx) Err() error { 
  10.  return nil 
  11.  
  12. func (*emptyCtx) Value(key interface{}) interface{} { 
  13.  return nil 

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

cancelCtx

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

  1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { 
  2.  c := newCancelCtx(parent) 
  3.  propagateCancel(parent, &c) 
  4.  return &c, func() { c.cancel(true, Canceled) } 
  5.  
  6. func newCancelCtx(parent Context) cancelCtx { 
  7.  return cancelCtx{Context: parent} 

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

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

 

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

接下來我們針對 cancelCtx 類型來進(jìn)一步看看:

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

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

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

  1. func (c *cancelCtx) Value(key interface{}) interface{} { 
  2.  if key == &cancelCtxKey { 
  3.   return c 
  4.  } 
  5.  return c.Context.Value(key
  6.  
  7. func (c *cancelCtx) Done() <-chan struct{} { 
  8.  c.mu.Lock() 
  9.  if c.done == nil { 
  10.   c.done = make(chan struct{}) 
  11.  } 
  12.  d := c.done 
  13.  c.mu.Unlock() 
  14.  return d 
  15.  
  16. func (c *cancelCtx) Err() error { 
  17.  c.mu.Lock() 
  18.  err := c.err 
  19.  c.mu.Unlock() 
  20.  return err 

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

timerCtx

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

  1. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { 
  2.  return WithDeadline(parent, time.Now().Add(timeout)) 
  3.  
  4. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { 
  5.  ... 
  6.  c := &timerCtx{ 
  7.   cancelCtx: newCancelCtx(parent), 
  8.   deadline:  d, 
  9.  } 

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

  1. type timerCtx struct { 
  2.  cancelCtx 
  3.  timer *time.Timer // Under cancelCtx.mu. 
  4.  
  5.  deadline time.Time 

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

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

  1. func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { 
  2.  return c.deadline, true 
  3.  
  4. func (c *timerCtx) cancel(removeFromParent bool, err error) { 
  5.  c.cancelCtx.cancel(false, err) 
  6.  if removeFromParent { 
  7.   removeChild(c.cancelCtx.Context, c) 
  8.  } 
  9.  c.mu.Lock() 
  10.  if c.timer != nil { 
  11.   c.timer.Stop() 
  12.   c.timer = nil 
  13.  } 
  14.  c.mu.Unlock() 

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

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

該方法會先進(jìn)行前置判斷,若父級節(jié)點(diǎn)的 Deadline 時(shí)間早于當(dāng)前所指定的 Deadline 時(shí)間,將會直接生成一個(gè) cancelCtx 的 context。

  1. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { 
  2.  ... 
  3.  c := &timerCtx{ 
  4.   cancelCtx: newCancelCtx(parent), 
  5.   deadline:  d, 
  6.  } 
  7.  propagateCancel(parent, c) 
  8.  dur := time.Until(d) 
  9.  if dur <= 0 { 
  10.   c.cancel(true, DeadlineExceeded) // deadline has already passed 
  11.   return c, func() { c.cancel(false, Canceled) } 
  12.  } 
  13.  c.mu.Lock() 
  14.  defer c.mu.Unlock() 
  15.  if c.err == nil { 
  16.   c.timer = time.AfterFunc(dur, func() { 
  17.    c.cancel(true, DeadlineExceeded) 
  18.   }) 
  19.  } 
  20.  return c, func() { c.cancel(true, Canceled) } 

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

valueCtx

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

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

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

  1. type valueCtx struct { 
  2.  Context 
  3.  key, val interface{} 

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

  1. func (c *valueCtx) Value(key interface{}) interface{} { 
  2.  if c.key == key { 
  3.   return c.val 
  4.  } 
  5.  return c.Context.Value(key

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

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

 

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

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

context 取消事件

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

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

  1. func propagateCancel(parent Context, child canceler) { 
  2.  done := parent.Done() 
  3.  if done == nil { 
  4.   return 
  5.  } 
  6.  
  7.  select { 
  8.  case <-done: 
  9.   child.cancel(false, parent.Err()) 
  10.   return 
  11.  default
  12.  } 
  13.  ... 
  • 當(dāng)父級上下文(parent)的 Done 結(jié)果為 nil 時(shí),將會直接返回,因?yàn)槠洳粫邆淙∠录幕緱l件,可能該 context 是 Background、TODO 等方法產(chǎn)生的空白 context。
  • 當(dāng)父級上下文(parent)的 Done 結(jié)果不為 nil 時(shí),則發(fā)現(xiàn)父級上下文已經(jīng)被取消,作為其子級,該 context 將會觸發(fā)取消事件并返回父級上下文的取消原因。
  1. func propagateCancel(parent Context, child canceler) { 
  2.  ... 
  3.  if p, ok := parentCancelCtx(parent); ok { 
  4.   p.mu.Lock() 
  5.   if p.err != nil { 
  6.    child.cancel(false, p.err) 
  7.   } else { 
  8.    if p.children == nil { 
  9.     p.children = make(map[canceler]struct{}) 
  10.    } 
  11.    p.children[child] = struct{}{} 
  12.   } 
  13.   p.mu.Unlock() 
  14.  } else { 
  15.   atomic.AddInt32(&goroutines, +1) 
  16.   go func() { 
  17.    select { 
  18.    case <-parent.Done(): 
  19.     child.cancel(false, parent.Err()) 
  20.    case <-child.Done(): 
  21.    } 
  22.   }() 
  23.  } 

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

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

  • 調(diào)用 parentCancelCtx 方法找到具備取消功能的父級 context。并將當(dāng)前 context,也就是 child 加入到 父級 context 的 children 列表中,等待后續(xù)父級 context 的取消事件通知和響應(yīng)。
  • 調(diào)用 parentCancelCtx 方法沒有找到,將會啟動一個(gè)新的 goroutine 去監(jiān)聽父子 context 的取消事件通知。

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

 

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

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

總結(jié)

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

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

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

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

 

責(zé)任編輯:武曉燕 來源: 腦子進(jìn)煎魚了
相關(guān)推薦

2025-03-18 09:10:00

MCPAI模型上下文協(xié)議

2025-01-08 11:10:46

2022-10-28 16:24:33

Context上下文鴻蒙

2025-04-07 05:01:00

MCP上下文協(xié)議LLM?

2022-05-03 21:01:10

架構(gòu)項(xiàng)目映射

2025-04-07 01:02:00

GoAPI語言

2017-05-11 14:00:02

Flask請求上下文應(yīng)用上下文

2024-08-27 09:46:39

Go協(xié)程效率

2015-10-09 09:43:28

CSS CSS3

2021-07-26 07:47:36

Cpu上下文進(jìn)程

2012-12-31 10:01:34

SELinuxSELinux安全

2025-02-08 09:13:40

2022-11-03 08:29:32

編程管理器協(xié)議

2024-04-26 00:01:00

Go語言類型

2022-09-14 13:13:51

JavaScript上下文

2024-12-03 12:02:05

2022-09-15 08:01:14

繼承基礎(chǔ)設(shè)施基礎(chǔ)服務(wù)

2023-11-01 08:08:50

Go語言傳遞請求

2024-09-18 13:57:15

2021-04-27 11:28:21

React.t事件元素
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號