詳解Context包,看這一篇就夠了!!!

前言

最近在項目開發時,常用到Context這個包。context.Context是Go語言中獨特的設計,在其餘編程語言中咱們不多見到相似的概念。因此這一期咱們就來好好講一講Context的基本概念與實際使用,麻麻不再擔憂個人併發編程啦~~~。html

什麼是context

在理解context包以前,咱們應該熟悉兩個概念,由於這能加深你對context的理解。git

1. Goroutine

Goroutine是一個輕量級的執行線程,多個Goroutine比一個線程輕量因此管理他們消耗的資源相對更少。Goroutine是Go中最基本的執行單元,每個Go程序至少有一個Goroutine:主Goroutine。程序啓動時會自動建立。這裏爲了你們能更好的理解Goroutine,咱們先來看一看線程與協程的概念。程序員

  • 線程(Thread)

線程是一種輕量級進程,是CPU調度的最小單位。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程本身不擁有系統資源,只擁有一點在運行中必不可少的資源,但它可與同屬於一個進程的其餘線程共享進程所擁有的所有資源。線程擁有本身獨立的棧和共享的堆,共享堆,不共享棧,線程的切換通常也由操做系統調度。github

  • 協程(coroutine)

又稱爲微線程與子例程同樣,協程也是一種程序組建,相對子例程而言,協程更爲靈活,但在實踐中使用沒有子例程那樣普遍。和線程相似,共享堆,不共享棧,協程的切換通常由程序員在代碼中顯式控制。他避免了上下文切換的額外耗費,兼顧了多線程的優勢,簡化了高併發程序的複雜。golang

Goroutine和其餘語言的協程(coroutine)在使用方式上相似,但從字面意義上來看不一樣(一個是Goroutine,一個是coroutine),再就是協程是一種協做任務控制機制,在最簡單的意義上,協程不是併發的,而Goroutine支持併發的。所以Goroutine能夠理解爲一種Go語言的協程。同時它能夠運行在一個或多個線程上。web

咱們來看一個簡單示例:面試

func Hello()  {
 fmt.Println("hello everybody , I'm asong")
}

func main()  {
 go Hello()
 fmt.Println("Golang夢工廠")
}

上面的程序,咱們使用go又開啓了一個Goroutine執行Hello方法,可是咱們運行這個程序,運行結果以下:數據庫

Golang夢工廠

這裏出現這個問題的緣由是咱們啓動的goroutinemain執行完就退出了,因此爲了main等待這個Goroutine執行完,咱們就須要一些方法,讓goroutine告訴main執行完了,這裏就須要通道了。編程

2. 通道

這是 goroutine 之間的溝通渠道。當您想要將結果或錯誤,或任何其餘類型的信息從一個 goroutine 傳遞到另外一個 goroutine 時就可使用通道。通道是有類型的,能夠是 int 類型的通道接收整數或錯誤類型的接收錯誤等。設計模式

假設有個 int 類型的通道 ch,若是你想發一些信息到這個通道,語法是 ch <- 1,若是你想從這個通道接收一些信息,語法就是 var := <-ch。這將從這個通道接收並存儲值到 var 變量。

如下程序說明了通道的使用確保了 goroutine 執行完成並將值返回給 main 。

func Hello(ch chan int)  {
 fmt.Println("hello everybody , I'm asong")
 ch <- 1
}

func main()  {
 ch := make(chan int)
 go Hello(ch)
 <-ch
 fmt.Println("Golang夢工廠")
}

這裏咱們使用通道進行等待,這樣main就會等待goroutine執行完。如今咱們知道了goroutinechannel的概念了,下面咱們就來介紹一下context

3. 場景

有了上面的概念,咱們在來看一個例子:

以下代碼,每次請求,Handler會建立一個goroutine來爲其提供服務,並且連續請求3次,request的地址也是不一樣的:

func main()  {
 http.HandleFunc("/", SayHello) // 設置訪問的路由

 log.Fatalln(http.ListenAndServe(":8080",nil))
}

func SayHello(writer http.ResponseWriter, request *http.Request)  {
 fmt.Println(&request)
 writer.Write([]byte("Hi"))
}

========================================================
$ curl http://localhost:8080/
0xc0000b8030
0xc000186008
0xc000186018

而每一個請求對應的Handler,常會啓動額外的的goroutine進行數據查詢或PRC調用等。

而當請求返回時,這些額外建立的goroutine須要及時回收。並且,一個請求對應一組請求域內的數據可能會被該請求調用鏈條內的各goroutine所須要。

如今咱們對上面代碼在添加一點東西,當請求進來時,Handler建立一個監控goroutine,這樣就會每隔1s打印一句Current request is in progress

func main()  {
 http.HandleFunc("/", SayHello) // 設置訪問的路由

 log.Fatalln(http.ListenAndServe(":8080",nil))
}

func SayHello(writer http.ResponseWriter, request *http.Request)  {
 fmt.Println(&request)

 go func() {
  for range time.Tick(time.Second) {
   fmt.Println("Current request is in progress")
  }
 }()

 time.Sleep(2 * time.Second)
 writer.Write([]byte("Hi"))
}

這裏我假定請求須要耗時2s,在請求2s後返回,咱們指望監控goroutine在打印2次Current request is in progress後即中止。但運行發現,監控goroutine打印2次後,其仍不會結束,而會一直打印下去。

問題出在建立監控goroutine後,未對其生命週期做控制,下面咱們使用context做一下控制,即監控程序打印前需檢測request.Context()是否已經結束,若結束則退出循環,即結束生命週期。

func main()  {
 http.HandleFunc("/", SayHello) // 設置訪問的路由

 log.Fatalln(http.ListenAndServe(":8080",nil))
}

func SayHello(writer http.ResponseWriter, request *http.Request)  {
 fmt.Println(&request)

 go func() {
  for range time.Tick(time.Second) {
   select {
   case <- request.Context().Done():
    fmt.Println("request is outgoing")
    return
   default:
    fmt.Println("Current request is in progress")
   }
  }
 }()

 time.Sleep(2 * time.Second)
 writer.Write([]byte("Hi"))
}

基於如上需求,context包應用而生。context包能夠提供一個請求從API請求邊界到各goroutine的請求域數據傳遞、取消信號及截至時間等能力。詳細原理請看下文。

4. context

在 Go 語言中 context 包容許您傳遞一個 "context" 到您的程序。Context 如超時或截止日期(deadline)或通道,來指示中止運行和返回。例如,若是您正在執行一個 web 請求或運行一個系統命令,定義一個超時對生產級系統一般是個好主意。由於,若是您依賴的API運行緩慢,你不但願在系統上備份(back up)請求,由於它可能最終會增長負載並下降全部請求的執行效率。致使級聯效應。這是超時或截止日期 context 派上用場的地方。

4.1 設計原理

Go 語言中的每個請求的都是經過一個單獨的 Goroutine 進行處理的,HTTP/RPC 請求的處理器每每都會啓動新的 Goroutine 訪問數據庫和 RPC 服務,咱們可能會建立多個 Goroutine 來處理一次請求,而 Context 的主要做用就是在不一樣的 Goroutine 之間同步請求特定的數據、取消信號以及處理請求的截止日期。

每個 Context 都會從最頂層的 Goroutine 一層一層傳遞到最下層,這也是 Golang 中上下文最多見的使用方式,若是沒有 Context,當上層執行的操做出現錯誤時,下層其實不會收到錯誤而是會繼續執行下去。

當最上層的 Goroutine 由於某些緣由執行失敗時,下兩層的 Goroutine 因爲沒有接收到這個信號因此會繼續工做;可是當咱們正確地使用 Context 時,就能夠在下層及時停掉無用的工做減小額外資源的消耗:

這其實就是 Golang 中上下文的最大做用,在不一樣 Goroutine 之間對信號進行同步避免對計算資源的浪費,與此同時 Context 還能攜帶以請求爲做用域的鍵值對信息。

這裏光說,其實也不能徹底理解其中的做用,因此咱們來看一個例子:

func main()  {
 ctx,cancel := context.WithTimeout(context.Background(),1 * time.Second)
 defer cancel()
 go HelloHandle(ctx,500*time.Millisecond)
 select {
 case <- ctx.Done():
  fmt.Println("Hello Handle ",ctx.Err())
 }
}

func HelloHandle(ctx context.Context,duration time.Duration)  {

 select {
 case <-ctx.Done():
  fmt.Println(ctx.Err())
 case <-time.After(duration):
  fmt.Println("process request with", duration)
 }

}

上面的代碼,由於過時時間大於處理時間,因此咱們有足夠的時間處理改請求,因此運行代碼以下圖所示:

process request with 500ms
Hello Handle  context deadline exceeded

HelloHandle函數並無進入超時的select分支,可是main函數的select卻會等待context.Context的超時並打印出Hello Handle context deadline exceeded。若是咱們將處理請求的時間增長至2000ms,程序就會由於上下文過時而被終止。

context deadline exceeded
Hello Handle  context deadline exceeded

4.2 接口

context.Context 是 Go 語言在 1.7 版本中引入標準庫的接口1,該接口定義了四個須要實現的方法,其中包括:

  • Deadline — 返回 context.Context 被取消的時間,也就是完成工做的截止日期;
  • Done — 返回一個 Channel,這個 Channel 會在當前工做完成或者上下文被取消以後關閉,屢次調用 Done 方法會返回同一個 Channel;
  • Err — 返回 context.Context 結束的緣由,它只會在 Done 返回的 Channel 被關閉時纔會返回非空的值;
    • 若是 context.Context 被取消,會返回 Canceled 錯誤;
    • 若是 context.Context 超時,會返回 DeadlineExceeded 錯誤;
  • Value — 從 context.Context 中獲取鍵對應的值,對於同一個上下文來講,屢次調用 Value 並傳入相同的 Key 會返回相同的結果,該方法能夠用來傳遞請求特定的數據;
type Context interface {
 Deadline() (deadline time.Time, ok bool)
 Done() <-chan struct{}
 Err() error
 Value(key interface{}) interface{}
}

context 使用詳解

建立context

context包容許如下方式建立和得到context:

  • context.Background():這個函數返回一個空 context。這隻能用於高等級(在 main 或頂級請求處理中)。
  • context.TODO():這個函數也是建立一個空 context。也只能用於高等級或當您不肯定使用什麼 context,或函數之後會更新以便接收一個 context 。這意味您(或維護者)計劃未來要添加 context 到函數。

其實咱們查看源代碼。發現他倆都是經過 new(emptyCtx) 語句初始化的,它們是指向私有結構體 context.emptyCtx 的指針,這是最簡單、最經常使用的上下文類型:

var (
 background = new(emptyCtx)
 todo       = new(emptyCtx)
)
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
}

func (e *emptyCtx) String() string {
 switch e {
 case background:
  return "context.Background"
 case todo:
  return "context.TODO"
 }
 return "unknown empty Context"
}

從上述代碼,咱們不難發現 context.emptyCtx 經過返回 nil 實現了 context.Context 接口,它沒有任何特殊的功能。

從源代碼來看,context.Backgroundcontext.TODO 函數其實也只是互爲別名,沒有太大的差異。它們只是在使用和語義上稍有不一樣:

  • context.Background 是上下文的默認值,全部其餘的上下文都應該從它衍生(Derived)出來。
  • context.TODO 應該只在不肯定應該使用哪一種上下文時使用;

在多數狀況下,若是當前函數沒有上下文做爲入參,咱們都會使用 context.Background 做爲起始的上下文向下傳遞。

context的繼承衍生

有了如上的根Context,那麼是如何衍生更多的子Context的呢?這就要靠context包爲咱們提供的With系列的函數了。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

這四個With函數,接收的都有一個partent參數,就是父Context,咱們要基於這個父Context建立出子Context的意思,這種方式能夠理解爲子Context對父Context的繼承,也能夠理解爲基於父Context的衍生。

經過這些函數,就建立了一顆Context樹,樹的每一個節點均可以有任意多個子節點,節點層級能夠有任意多個。

WithCancel函數,傳遞一個父Context做爲參數,返回子Context,以及一個取消函數用來取消Context。WithDeadline函數,和WithCancel差很少,它會多傳遞一個截止時間參數,意味着到了這個時間點,會自動取消Context,固然咱們也能夠不等到這個時候,能夠提早經過取消函數進行取消。

WithTimeoutWithDeadline基本上同樣,這個表示是超時自動取消,是多少時間後自動取消Context的意思。

WithValue函數和取消Context無關,它是爲了生成一個綁定了一個鍵值對數據的Context,這個綁定的數據能夠經過Context.Value方法訪問到,後面咱們會專門講。

你們可能留意到,前三個函數都返回一個取消函數CancelFunc,這是一個函數類型,它的定義很是簡單。

type CancelFunc func()

這就是取消函數的類型,該函數能夠取消一個Context,以及這個節點Context下全部的全部的Context,無論有多少層級。

下面我就展開來介紹一個每個方法的使用。

WithValue

context 包中的 context.WithValue 函數能從父上下文中建立一個子上下文,傳值的子上下文使用 context.valueCtx 類型,咱們看一下源碼:

// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {
 if key == nil {
  panic("nil key")
 }
 if !reflectlite.TypeOf(key).Comparable() {
  panic("key is not comparable")
 }
 return &valueCtx{parent, key, val}
}
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
 Context
 key, val interface{}
}

// stringify tries a bit to stringify v, without using fmt, since we don't
// want context depending on the unicode tables. This is only used by
// *valueCtx.String().
func stringify(v interface{}) string {
 switch s := v.(type) {
 case stringer:
  return s.String()
 case string:
  return s
 }
 return "<not Stringer>"
}

func (c *valueCtx) String() string {
 return contextName(c.Context) + ".WithValue(type " +
  reflectlite.TypeOf(c.key).String() +
  ", val " + stringify(c.val) + ")"
}

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

此函數接收 context 並返回派生 context,其中值 val 與 key 關聯,並經過 context 樹與 context 一塊兒傳遞。這意味着一旦得到帶有值的 context,從中派生的任何 context 都會得到此值。不建議使用 context 值傳遞關鍵參數,而是函數應接收簽名中的那些值,使其顯式化。

context.valueCtx 結構體會將除了 Value 以外的 ErrDeadline 等方法代理到父上下文中,它只會響應 context.valueCtx.Value 方法。若是 context.valueCtx 中存儲的鍵值對與 context.valueCtx.Value 方法中傳入的參數不匹配,就會從父上下文中查找該鍵對應的值直到在某個父上下文中返回 nil 或者查找到對應的值。

說了這麼多,比較枯燥,咱們來看一下怎麼使用:

type key string

func main()  {
 ctx := context.WithValue(context.Background(),key("asong"),"Golang夢工廠")
 Get(ctx,key("asong"))
 Get(ctx,key("song"))
}

func Get(ctx context.Context,k key)  {
 if v, ok := ctx.Value(k).(string); ok {
  fmt.Println(v)
 }
}

上面代碼咱們基於context.Background建立一個帶值的ctx,而後能夠根據key來取值。這裏爲了不多個包同時使用context而帶來衝突,key不建議使用string或其餘內置類型,因此建議自定義key類型.

WithCancel

此函數建立從傳入的父 context 派生的新 context。父 context 能夠是後臺 context 或傳遞給函數的 context。返回派生 context 和取消函數。只有建立它的函數才能調用取消函數來取消此 context。若是您願意,能夠傳遞取消函數,可是,強烈建議不要這樣作。這可能致使取消函數的調用者沒有意識到取消 context 的下游影響。可能存在源自此的其餘 context,這可能致使程序以意外的方式運行。簡而言之,永遠不要傳遞取消函數。

咱們直接從 context.WithCancel 函數的實現來看它到底作了什麼:

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
 c := newCancelCtx(parent)
 propagateCancel(parent, &c)
 return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
 return cancelCtx{Context: parent}
}
  • context.newCancelCtx 將傳入的上下文包裝成私有結構體 context.cancelCtx
  • context.propagateCancel 會構建父子上下文之間的關聯,當父上下文被取消時,子上下文也會被取消:
func propagateCancel(parent Context, child canceler) {
 done := parent.Done()
 if done == nil {
  return // 父上下文不會觸發取消信號
 }
 select {
 case <-done:
  child.cancel(false, parent.Err()) // 父上下文已經被取消
  return
 default:
 }

 if p, ok := parentCancelCtx(parent); ok {
  p.mu.Lock()
  if p.err != nil {
   child.cancel(false, p.err)
  } else {
   p.children[child] = struct{}{}
  }
  p.mu.Unlock()
 } else {
  go func() {
   select {
   case <-parent.Done():
    child.cancel(false, parent.Err())
   case <-child.Done():
   }
  }()
 }
}

上述函數總共與父上下文相關的三種不一樣的狀況:

  1. parent.Done() == nil,也就是 parent 不會觸發取消事件時,當前函數會直接返回;
  2. child 的繼承鏈包含能夠取消的上下文時,會判斷 parent 是否已經觸發了取消信號;
    • 若是已經被取消, child 會馬上被取消;
    • 若是沒有被取消, child 會被加入 parentchildren 列表中,等待 parent 釋放取消信號;
  3. 在默認狀況下
    • 運行一個新的 Goroutine 同時監聽 parent.Done()child.Done() 兩個 Channel
    • parent.Done() 關閉時調用 child.cancel 取消子上下文;

context.propagateCancel 的做用是在 parentchild 之間同步取消和結束的信號,保證在 parent 被取消時,child 也會收到對應的信號,不會發生狀態不一致的問題。

context.cancelCtx 實現的幾個接口方法也沒有太多值得分析的地方,該結構體最重要的方法是 cancel,這個方法會關閉上下文中的 Channel 並向全部的子上下文同步取消信號:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
 c.mu.Lock()
 if c.err != nil {
  c.mu.Unlock()
  return
 }
 c.err = err
 if c.done == nil {
  c.done = closedchan
 } else {
  close(c.done)
 }
 for child := range c.children {
  child.cancel(false, err)
 }
 c.children = nil
 c.mu.Unlock()

 if removeFromParent {
  removeChild(c.Context, c)
 }
}

說了這麼,看一例子,帶你感覺一下使用方法:

func main()  {
 ctx,cancel := context.WithCancel(context.Background())
 defer cancel()
 go Speak(ctx)
 time.Sleep(10*time.Second)
}

func Speak(ctx context.Context)  {
 for range time.Tick(time.Second){
  select {
  case <- ctx.Done():
   return
  default:
   fmt.Println("balabalabalabala")
  }
 }
}

咱們使用withCancel建立一個基於Background的ctx,而後啓動一個講話程序,每隔1s說一話,main函數在10s後執行cancel,那麼speak檢測到取消信號就會退出。

WithDeadline

此函數返回其父項的派生 context,當截止日期超過或取消函數被調用時,該 context 將被取消。例如,您能夠建立一個將在之後的某個時間自動取消的 context,並在子函數中傳遞它。當由於截止日期耗盡而取消該 context 時,獲此 context 的全部函數都會收到通知去中止運行並返回。

咱們來看一下源碼:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
 if cur, ok := parent.Deadline(); ok && cur.Before(d) {
  return WithCancel(parent)
 }
 c := &timerCtx{
  cancelCtx: newCancelCtx(parent),
  deadline:  d,
 }
 propagateCancel(parent, c)
 dur := time.Until(d)
 if dur <= 0 {
  c.cancel(true, DeadlineExceeded) // 已通過了截止日期
  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) }
}

context.WithDeadline也都能建立能夠被取消的計時器上下文 context.timerCtx

context.WithDeadline 方法在建立 context.timerCtx 的過程當中,判斷了父上下文的截止日期與當前日期,並經過 time.AfterFunc 建立定時器,當時間超過了截止日期後會調用 context.timerCtx.cancel 方法同步取消信號。

context.timerCtx 結構體內部不只經過嵌入了context.cancelCtx 結構體繼承了相關的變量和方法,還經過持有的定時器 timer 和截止時間 deadline 實現了定時取消這一功能:

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

 deadline time.Time
}

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()
}

context.timerCtx.cancel 方法不只調用了 context.cancelCtx.cancel,還會中止持有的定時器減小沒必要要的資源浪費。

接下來咱們來看一個例子:

func main()  {
 now := time.Now()
 later,_:=time.ParseDuration("10s")
 
 ctx,cancel := context.WithDeadline(context.Background(),now.Add(later))
 defer cancel()
 go Monitor(ctx)

 time.Sleep(20 * time.Second)

}

func Monitor(ctx context.Context)  {
 select {
 case <- ctx.Done():
  fmt.Println(ctx.Err())
 case <-time.After(20*time.Second):
  fmt.Println("stop monitor")
 }
}

設置一個監控goroutine,使用WithTimeout建立一個基於Background的ctx,其會當前時間的10s後取消。驗證結果以下:

context deadline exceeded

10s,監控goroutine被取消了。

WithTimeout

此函數相似於 context.WithDeadline。不一樣之處在於它將持續時間做爲參數輸入而不是時間對象。此函數返回派生 context,若是調用取消函數或超出超時持續時間,則會取消該派生 context。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
 return WithDeadline(parent, time.Now().Add(timeout))
}

觀看源碼咱們能夠看出WithTimeout內部調用的就是WithDeadline,其原理都是同樣的,上面已經介紹過了,來看一個例子吧:

func main()  {

 ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)
 defer cancel()
 go Monitor(ctx)

 time.Sleep(20 * time.Second)

}

func Monitor(ctx context.Context)  {
 select {
 case <- ctx.Done():
  fmt.Println(ctx.Err())
 case <-time.After(20*time.Second):
  fmt.Println("stop monitor")
 }
}

Context使用原則

  • context.Background 只應用在最高等級,做爲全部派生 context 的根。

  • context 取消是建議性的,這些函數可能須要一些時間來清理和退出。

  • 不要把Context放在結構體中,要以參數的方式傳遞。

  • Context做爲參數的函數方法,應該把Context做爲第一個參數,放在第一位。

  • 給一個函數方法傳遞Context的時候,不要傳遞nil,若是不知道傳遞什麼,就使用context.TODO

  • Context的Value相關方法應該傳遞必須的數據,不要什麼數據都使用這個傳遞。context.Value 應該不多使用,它不該該被用來傳遞可選參數。這使得 API 隱式的而且能夠引發錯誤。取而代之的是,這些值應該做爲參數傳遞。

  • Context是線程安全的,能夠放心的在多個goroutine中傳遞。同一個Context能夠傳給使用其的多個goroutine,且Context可被多個goroutine同時安全訪問。

  • Context 結構沒有取消方法,由於只有派生 context 的函數才應該取消 context。

Go 語言中的 context.Context 的主要做用仍是在多個 Goroutine 組成的樹中同步取消信號以減小對資源的消耗和佔用,雖然它也有傳值的功能,可是這個功能咱們仍是不多用到。在真正使用傳值的功能時咱們也應該很是謹慎,使用 context.Context 進行傳遞參數請求的全部參數一種很是差的設計,比較常見的使用場景是傳遞請求對應用戶的認證令牌以及用於進行分佈式追蹤的請求 ID。

總結

好啦,這一期文章到這裏就結束啦。爲了弄懂這裏,參考不少文章,會在結尾貼出來,供你們學習參考。由於這個包真的很重要,在日常項目開發中咱們也是常用到,因此你們弄懂context的原理仍是頗有必要的。

文章的示例代碼已上傳github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/context_example

有須要的小夥伴能夠下在觀看學習,若是再能給個小星星就很是感謝了。

結尾給你們發一個小福利吧,最近我在看[微服務架構設計模式]這一本書,講的很好,本身也收集了一本PDF,有須要的小夥能夠到自行下載。獲取方式:關注公衆號:[Golang夢工廠],後臺回覆:[微服務],便可獲取。

我翻譯了一份GIN中文文檔,會按期進行維護,有須要的小夥伴後臺回覆[gin]便可下載。

我是asong,一名普普統統的程序猿,讓我一塊兒慢慢變強吧。我本身建了一個golang交流羣,有須要的小夥伴加我vx,我拉你入羣。歡迎各位的關注,咱們下期見~~~

推薦往期文章:

參考文章

  • https://leileiluoluo.com/posts/golang-context.html
  • https://studygolang.com/articles/13866
  • https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/


本文分享自微信公衆號 - Golang夢工廠(AsongDream)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索