日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

一篇文章搞懂Go語言中的Context_Golang

作者:??BarryYan???? ? 更新時間: 2022-08-26 編程語言

0 前置知識sync.WaitGroup

sync.WaitGroup是等待一組協程結束。它實現了一個類似任務隊列的結構,可以向隊列中加入任務,任務完成后就把任務從隊列中移除,如果隊列中的任務沒有全部完成,隊列就會觸發阻塞以阻止程序繼續運行。?sync.WaitGroup只有3個方法,Add(),Done(),Wait()?。

其中Done()是Add(-1)的別名,使用Add()添加計數,Done()減掉一個計數,計數不為0, 阻塞Wait()的運行。

示例:

package main
import (
 ? "fmt"
 ? "sync"
 ? "time"
)
var group sync.WaitGroup
func sayHello() {
 ? for i := 0; i < 5; i++ {
 ? ? ?fmt.Println("hello......")
 ? ? ?time.Sleep(time.Second)
 ? }
 ? //線程結束 -1
 ? group.Done()
}
func sayHi() {
 ? //線程結束 -1
 ? defer group.Done()
 ? for i := 0; i < 5; i++ {
 ? ? ?fmt.Println("hi......")
 ? ? ?time.Sleep(time.Second)
 ? }
}
func main() {
 ? //+2
 ? group.Add(2)
 ? fmt.Println("main正在阻塞...")
 ? go sayHello()
 ? fmt.Println("main持續阻塞...")
 ? go sayHi()
 ? //線程等待
 ? group.Wait()
 ? fmt.Println("main貌似結束了阻塞...")
}

效果:

1 簡介

在 Go 服務器中,每個傳入請求都在其自己的 goroutine 中處理。請求處理程序通常會啟動額外的 goroutine 來訪問后端,例如數據庫和 RPC 服務。處理請求的一組 goroutine 通常需要訪問特定于請求的值,例如最終用戶的身份、授權令牌和請求的截止日期。當請求被取消或超時時,處理該請求的所有 goroutine 都應該快速退出,以便系統可以回收它們正在使用的任何資源。

為此,開發了一個context包,可以輕松地將請求范圍的值、取消信號和截止日期跨 API 邊界傳遞給處理請求所涉及的所有 goroutine。

Context攜帶一個截止日期、一個取消信號和其他跨越API邊界的值。上下文的方法可以被多個gor例程同時調用。

對服務器的傳入請求應該創建一個上下文,對服務器的傳出調用應該接受一個上下文。它們之間的函數調用鏈必須傳播 Context,可選擇將其替換為使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 創建的派生 Context。當一個上下文被取消時,所有從它派生的上下文也被取消。

WithCancel、WithDeadline 和 WithTimeout 函數采用 Context(父)并返回派生的 Context(子)和 CancelFunc。調用 CancelFunc 會取消子項及其子項,刪除父項對子項的引用,并停止任何關聯的計時器。調用 CancelFunc 失敗會泄漏子項及其子項,直到父項被取消或計時器觸發。go vet 工具檢查是否在所有控制流路徑上使用了 CancelFuncs。

使用上下文的程序應遵循以下規則,以保持跨包的接口一致,并啟用靜態分析工具來檢查上下文傳播:

不要將上下文存儲在結構類型中;相反,將 Context 顯式傳遞給需要它的每個函數。

Context 應該是第一個參數,通常命名為 ctx:

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

即使函數允許,也不要傳遞 nil 上下文。如果不確定要使用哪個 Context,請傳遞 context.TODO。

僅將上下文值用于傳輸流程和 API 的請求范圍數據,而不用于將可選參數傳遞給函數。

相同的 Context 可以傳遞給在不同的 goroutine 中運行的函數;上下文對于多個 goroutine 同時使用是安全的。

2 context.Context引入

//上下文攜帶截止日期、取消信號和請求范圍的值在API的界限。它的方法是安全的同時使用多個了goroutine。
type Context interface {
 ? ?// Done返回一個在上下文被取消或超時時關閉的通道。
 ? ?Done() <-chan struct{}
?
 ? ?// Err表示在Done通道關閉后為何取消此上下文。
 ? ?Err() error
?
 ? ?// Deadline返回上下文將被取消的時間(如果有的話)。
 ? ?Deadline() (deadline time.Time, ok bool)
?
 ? ?// Value返回與key相關的值,如果沒有則返回nil。
 ? ?Value(key interface{}) interface{}
}
  • Done方法返回一個通道,該通道作為代表運行的函數的取消信號Context:當通道關閉時,函數應該放棄它們的工作并返回。
  • Err方法返回一個錯誤,指示Context取消的原因。
  • 一個Context對于多個 goroutine 同時使用是安全的。代碼可以將單個傳遞Context給任意數量的 goroutines 并取消它Context以向所有goroutine 發出信號。
  • Deadline方法允許函數確定它們是否應該開始工作,還可以使用截止日期來設置 I/O 操作的超時時間。
  • Value允許一個Context攜帶請求范圍的數據。該數據必須是安全的,以便多個 goroutine 同時使用。

3 context包的其他常用函數

3.1 context.Background和context.TODO

Background是任何Context樹的根,它永遠不會被取消:

//Background返回一個空的Context。 它永遠不會取消,沒有截止日期,沒有價值。 Background通常用于main、init和tests,并作為傳入請求的頂級上下文。 ?
func Background() Context

給一個函數方法傳遞Context的時候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO()

3.2 context.WithCancel和

WithCancelt返回派生的Context值,可以比父Context更快地取消。當請求處理程序返回時,通常會取消與傳入請求關聯的content。當使用多個副本時,WithCancel對于取消冗余請求也很有用。

// WithCancel返回一個父進程的副本,該父進程的Done通道被盡快關閉。?關閉Done或調用cancel。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// CancelFunc取消一個上下文。
type CancelFunc func()

示例:

package main
import (
 ? "context"
 ? "fmt"
)
func play(ctx context.Context) <-chan int {
 ? dist := make(chan int)
 ? n := 1
 ? //匿名函數 向dist中加入元素
 ? go func() {
 ? ? ?for {
 ? ? ? ? select {
 ? ? ? ? //ctx為空時將不會執行這個
 ? ? ? ? case <-ctx.Done():
 ? ? ? ? ? ?return // return結束該goroutine,防止泄露
 ? ? ? ? ? ?//向dist中加入元素
 ? ? ? ? case dist <- n:
 ? ? ? ? ? ?n++
 ? ? ? ? }
 ? ?  }
 ? }()
 ? return dist
}
func main() {
 ? //返回空的context
 ? ctx, cancel := context.WithCancel(context.Background())
 ? defer cancel() // 調用cancel
 ? for n := range play(ctx) {
 ? ? ?fmt.Println(n)
 ? ? ?if n == 5 {
 ? ? ? ? break
 ? ?  }
 ? }
}

擴展:go中select的用法

```
select的用法與switch語言非常類似,由select開始一個新的選擇塊,每個選擇條件由case語句來描述。
與switch語句相比, select有比較多的限制,其中最大的一條限制就是每個case語句里必須是一個IO操作,大致的結構如下:
``` go
select {
 ? case <-chan1:
 ? ? ?// 如果chan1成功讀到數據,則進行該case處理語句
 ? case chan2 <- 1:
 ? ? ?// 如果成功向chan2寫入數據,則進行該case處理語句
 ? default:
 ? ? ?// 如果上面都沒有成功,則進入default處理流程
}
```
在一個select語句中,Go語言會按順序從頭至尾評估每一個發送和接收的語句。
如果其中的任意一語句可以繼續執行(即沒有被阻塞),那么就從那些可以執行的語句中任意選擇一條來使用。
如果沒有任意一條語句可以執行(即所有的通道都被阻塞),那么有兩種可能的情況:
- 如果給出了default語句,那么就會執行default語句,同時程序的執行會從select語句后的語句中恢復。
- 如果沒有default語句,那么select語句將被阻塞,直到至少有一個通信可以進行下去。
```

3.3 context.WithTimeout

WithTimeout返回派生的Context值,WithTimeout用于設置請求到后端服務器的截止日期:

//WithTimeout返回一個父進程的副本,該父進程的Done通道被立即關閉的父母。關閉“完成”、調用“取消”或超時結束。新
//Context的Deadline是現在的更快+timeout和父的Deadline,如果任何。?如果計時器仍然在運行,則cancel函數釋放它資源。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// CancelFunc取消一個上下文。
type CancelFunc func()

示例:

package main
import (
 ? "context"
 ? "fmt"
 ? "sync"
 ? "time"
)
var wg sync.WaitGroup
func worker(ctx context.Context) {
 ? ?LOOP:
 ? for {
 ? ? ?fmt.Println("db connecting ...")
 ? ? ?time.Sleep(time.Millisecond * 10) // 假設正常連接數據庫耗時10毫秒
 ? ? ?select {
 ? ? ?case <-ctx.Done(): // 50毫秒后自動調用
 ? ? ? ? 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")
}

執行結果:

3.4 context.WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
 ? if parent == nil {
 ? ? ?panic("cannot create context from nil parent")
 ? }
 ? 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) }
}

示例:

package main
import (
 ? "context"
 ? "fmt"
 ? "time"
)
func main() {
 ? d := time.Now().Add(500 * time.Millisecond)
 ? ctx, cancel := context.WithDeadline(context.Background(), d)
 ? // 盡管ctx會過期,但在任何情況下調用它的cancel函數都是很好的實踐。
 ? // 如果不這樣做,可能會使上下文及其父類存活的時間超過必要的時間。
 ? defer cancel()
 ? select {
 ? case <-time.After(1 * time.Second):
 ? ? ?fmt.Println("over")
 ? case <-ctx.Done():
 ? ? ?fmt.Println(ctx.Err())
 ? }
}

執行結果:

3.5 context.WithValue

WithValue提供了一種將請求范圍的值與Context關聯的方法 :

//WithValue返回父元素的副本,其Value方法返回val for key。
func WithValue(parent Context, key interface{}, val interface{}) Context

了解如何使用context包的最好方法是通過一個已工作的示例。

示例:

package main
import (
 ? "context"
 ? "fmt"
 ? "sync"
 ? "time"
)
type TraceCode string
var wg sync.WaitGroup
func worker(ctx context.Context) {
 ? key := TraceCode("KEY_CODE")
 ? traceCode, ok := ctx.Value(key).(string) // 在子goroutine中獲取trace code
 ? if !ok {
 ? ? ?fmt.Println("invalid trace code")
 ? }
 ? ?LOOP:
 ? for {
 ? ? ?fmt.Printf("worker,code:%s\n", traceCode)
 ? ? ?time.Sleep(time.Millisecond * 10) // 假設正常連接數據庫耗時10毫秒
 ? ? ?select {
 ? ? ?case <-ctx.Done(): // 50毫秒后自動調用
 ? ? ? ? break LOOP
 ? ? ?default:
 ? ?  }
 ? }
 ? fmt.Println("worker is over!")
 ? wg.Done()
}
?
func main() {
 ? // 設置一個50毫秒的超時
 ? ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
 ? // 在系統的入口中設置trace code傳遞給后續啟動的goroutine實現日志數據聚合
 ? ctx = context.WithValue(ctx, TraceCode("KEY_CODE"), "12512312234")
 ? wg.Add(1)
 ? go worker(ctx)
 ? time.Sleep(time.Second * 5)
 ? cancel() // 通知子goroutine結束
 ? wg.Wait()
 ? fmt.Println("over")
}

執行結果:

4 實例:請求瀏覽器超時

server端:

package main
import (
 ? "fmt"
 ? "math/rand"
 ? "net/http"
 ? "time"
)
// server端,隨機出現慢響應
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(":9999", nil)
 ? if err != nil {
 ? ? ?panic(err)
 ? }
}

client端:

package main
import (
 ? "context"
 ? "fmt"
 ? "io/ioutil"
 ? "net/http"
 ? "sync"
 ? "time"
)
// 客戶端
?
type respData struct {
 ? resp *http.Response
 ? err ?error
}
func doCall(ctx context.Context) {
 ? // http長連接
 ? transport := http.Transport{DisableKeepAlives: true}
 ? client := http.Client{Transport: &transport}
?
 ? respChan := make(chan *respData, 1)
 ? req, err := http.NewRequest("GET", "http://127.0.0.1:9999/", nil)
 ? if err != nil {
 ? ? ?fmt.Println(err)
 ? ? ?return
 ? }
 ? req = req.WithContext(ctx) // 使用帶超時的ctx創建一個新的client request
 ? var wg sync.WaitGroup
 ? wg.Add(1)
 ? defer wg.Wait()
 ? go func() {
 ? ? ?resp, err := client.Do(req)
 ? ? ?fmt.Printf("resp:%v, err:%v\n", resp, err)
 ? ? ?rd := &respData{
 ? ? ? ? resp: resp,
 ? ? ? ? err: ?err,
 ? ?  }
 ? ? ?respChan <- rd
 ? ? ?wg.Done()
 ? }()
 ? select {
 ? case <-ctx.Done():
 ? ? ?fmt.Println("timeout...")
 ? case result := <-respChan:
 ? ? ?fmt.Println("success....")
 ? ? ?if result.err != nil {
 ? ? ? ? fmt.Printf("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() // 調用cancel釋放子goroutine資源
 ? doCall(ctx)
}

5 Context包都在哪些地方使用

許多服務器框架提供了用于承載請求作用域值的包和類型。我們可以定義“Context”接口的新實現,在使用現有框架的代碼和需要“Context”參數的代碼之間架起橋梁。

6 小結

在谷歌中,要求Go程序員將“Context”參數作為傳入和傳出請求之間的調用路徑上的每個函數的第一個參數傳遞。這使得許多不同團隊開發的Go代碼能夠很好地互操作。它提供了對超時和取消的簡單控制,并確保像安全憑證這樣的關鍵值能夠正確地傳輸Go程序。

想要構建在“Context”上的服務器框架應該提供“Context”的實現來連接它們的包和那些需要“Context”參數的包。然后,它們的客戶端庫將接受來自調用代碼的“Context”。通過為請求范圍的數據和取消建立一個公共接口,“上下文”使包開發人員更容易共享創建可伸縮服務的代碼。

原文鏈接:https://juejin.cn/post/7115913756596502565

欄目分類
最近更新