GO語言Context的作用及各種使用方法
Context
為什么需要Context
Go語言需要Context主要是為了在并發(fā)環(huán)境中有效地管理請求的上下文信息。Context提供了在函數(shù)之間傳遞取消信號、超時、截止時間等元數(shù)據(jù)的一種標(biāo)準(zhǔn)方式。
原因
- 取消操作: 在并發(fā)環(huán)境中,當(dāng)一個請求被取消或者超時時,需要有效地通知相關(guān)的協(xié)程停止正在進行的工作。使用Context可以通過傳遞取消信號來實現(xiàn)這一點。
- 超時控制: 在一些場景下,限制操作執(zhí)行的時間是很重要的。Context提供了一個統(tǒng)一的方式來處理超時,確保在規(guī)定的時間內(nèi)完成操作,防止程序無限期地等待。
- 傳遞上下文信息: Context可以用于傳遞請求的元數(shù)據(jù),例如請求的ID、用戶信息等。這在跨多個函數(shù)調(diào)用的情況下非常有用,避免了在函數(shù)參數(shù)中傳遞大量的上下文信息。
- 協(xié)程之間的通信: Go語言中的協(xié)程(goroutine)是輕量級的線程,它們之間需要有效地通信。Context提供了一個標(biāo)準(zhǔn)的方式來傳遞信號和元數(shù)據(jù),以便協(xié)程之間協(xié)同工作。
- 資源管理: 在一些場景下,需要確保在函數(shù)執(zhí)行完畢后釋放相關(guān)的資源,不管函數(shù)是正常執(zhí)行還是因為取消或超時而提前退出。Context可以幫助在正確的時機釋放資源。
- 綜上所述,Context是Go語言中處理并發(fā)、超時和取消等問題的一種優(yōu)雅而一致的方式,使得代碼更加健壯、可維護,并且更容易在不同的并發(fā)場景中工作。
多任務(wù)超時例子
我們都知道在go語言并發(fā)編程中,我們可以采用select來監(jiān)聽協(xié)程的的通道控制協(xié)程,但是如下面的這種情況僅僅憑借select就顯得有些無能為力:
- 支持多級嵌套,父任務(wù)停止后,子任務(wù)自動停止
- 控制停止順序,先停EFG 再停BCD 最后停A

目標(biāo)1還好說,目標(biāo)2好像就沒那么靈活了,正式討論context如何解決這些問題前,我們先看下常規(guī)context的使用
Context結(jié)構(gòu)
context 包是 Go 語言中用于處理請求的上下文的標(biāo)準(zhǔn)庫之一。它提供了一種在函數(shù)之間傳遞取消信號、超時和截止時間的機制。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline()返回一個完成工作的截止時間,表示上下文應(yīng)該被取消的時間。如果 ok==false 表示沒有設(shè)置截止時間。Done()返回一個 Channel,這個 Channel 會在當(dāng)前工作完成時被關(guān)閉,表示上下文應(yīng)該被取消。如果無法取消此上下文,則 Done 可能返回 nil。多次調(diào)用 Done 方法會返回同一個 Channel。Err()返回 Context 結(jié)束的原因,它只會在 Done 方法對應(yīng)的 Channel 關(guān)閉時返回非空值。如果 Context 被取消,會返回context.Canceled 錯誤;如果 Context 超時,會返回context.DeadlineExceeded錯誤。Value()從 Context 中獲取鍵對應(yīng)的值。如果未設(shè)置 key 對應(yīng)的值則返回 nil。以相同 key 多次調(diào)用會返回相同的結(jié)果。
另外,context 包中提供了兩個創(chuàng)建默認(rèn)上下文的函數(shù):
// TODO 返回一個非 nil 但空的上下文。 // 當(dāng)不清楚要使用哪種上下文或無可用上下文尚應(yīng)使用 context.TODO。 func TODO() Context // Background 返回一個非 nil 但空的上下文。 // 它不會被 cancel,沒有值,也沒有截止時間。它通常由 main 函數(shù)、初始化和測試使用,并作為處理請求的頂級上下文。 func Background() Context
還有四個基于父級創(chuàng)建不同類型上下文的函數(shù):
// WithCancel 基于父級創(chuàng)建一個具有 Done channel 的 context
func WithCancel(parent Context) (Context, CancelFunc)
// WithDeadline 基于父級創(chuàng)建一個不晚于 d 結(jié)束的 context
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
// WithTimeout 等同于 WithDeadline(parent, time.Now().Add(timeout))
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// WithValue 基于父級創(chuàng)建一個包含指定 key 和 value 的 context
func WithValue(parent Context, key, val interface{}) Context
在后面會詳細介紹這些不同類型 context 的用法。
Context各種使用方法
創(chuàng)建context
context包主要提供了兩種方式創(chuàng)建context:
- context.Backgroud()
- context.TODO()
這兩個函數(shù)其實只是互為別名,沒有差別,官方給的定義是:
- context.Background 是上下文的默認(rèn)值,所有其他的上下文都應(yīng)該從它衍生(Derived)出來。
- context.TODO 應(yīng)該只在不確定應(yīng)該使用哪種上下文時使用;
所以在大多數(shù)情況下,我們都使用context.Background作為起始的上下文向下傳遞。
上面的兩種方式是創(chuàng)建根context,不具備任何功能,具體實踐還是要依靠context包提供的With系列函數(shù)來進行派生:
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
valueCtx
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)
}
valueCtx利用一個Context類型的變量來表示父節(jié)點context,所以當(dāng)前context繼承了父context的所有信息;valueCtx類型還攜帶一組鍵值對,也就是說這種context可以攜帶額外的信息。valueCtx實現(xiàn)了Value方法,用以在context鏈路上獲取key對應(yīng)的值,如果當(dāng)前context上不存在需要的key,會沿著context鏈向上尋找key對應(yīng)的值,直到根節(jié)點。
WithValue
我們?nèi)粘T跇I(yè)務(wù)開發(fā)中都希望能有一個trace_id能串聯(lián)所有的日志,這就需要我們打印日志時能夠獲取到這個trace_id,在python中我們可以用gevent.local來傳遞,在java中我們可以用ThreadLocal來傳遞,在Go語言中我們就可以使用Context來傳遞,通過使用WithValue來創(chuàng)建一個攜帶trace_id的context,然后不斷透傳下去,打印日志時輸出即可,來看使用例子:
package main
import (
"context"
"fmt" // 我們需要使用fmt包中的Println()函數(shù)
"strings"
"time"
"github.com/google/uuid"
)
const (
KEY = "trace_id"
)
// 生成隨機ID
func NewRequestID() string {
return strings.Replace(uuid.New().String(), "-", "", -1)
}
// 生成攜帶值的context
func NewContextWithTraceID() context.Context {
ctx := context.WithValue(context.Background(), KEY, NewRequestID())
return ctx
}
//打印數(shù)據(jù)
func PrintLog(ctx context.Context, message string) {
fmt.Printf("%s|info|trace_id=%s|%s", time.Now().Format("2006-01-02 15:04:05"), GetContextValue(ctx, KEY), message)
}
// 獲取context中的值
func GetContextValue(ctx context.Context, k string) string {
v, ok := ctx.Value(k).(string)
if !ok {
return ""
}
return v
}
func ProcessEnter(ctx context.Context) {
PrintLog(ctx, "Golang夢工廠")
}
func main() {
ProcessEnter(NewContextWithTraceID())
}
結(jié)果
2024-01-10 18:55:03|info|trace_id=c4eeb76d427449fda52a4775ccbc0509|Golang夢工廠
cancelCtx
cancelCtx結(jié)構(gòu)體
type cancelCtx struct {
Context
mu sync.Mutex // 同步鎖,保護下面的所有字段
done chan struct{} //惰性創(chuàng)建,由第一次取消調(diào)用關(guān)閉
children map[canceler]struct{} // 在第一次取消調(diào)用時,設(shè)置為 nil
err error // 在第一次取消調(diào)用時設(shè)置為 non-nil
}
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
跟valueCtx類似,cancelCtx中也有一個context變量作為父節(jié)點;變量done表示一個channel,用來表示傳遞關(guān)閉信號;children表示一個map,存儲了當(dāng)前context節(jié)點下的子節(jié)點;err用于存儲錯誤信息表示任務(wù)結(jié)束的原因。
withCancel
日常業(yè)務(wù)開發(fā)中我們往往為了完成一個復(fù)雜的需求會開多個gouroutine去做一些事情,這就導(dǎo)致我們會在一次請求中開了多個goroutine確無法控制他們,這時我們就可以使用withCancel來衍生一個context傳遞到不同的goroutine中,當(dāng)我想讓這些goroutine停止運行,就可以調(diào)用cancel來進行取消。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go Speak(ctx)
time.Sleep(10 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
func Speak(ctx context.Context) {
for range time.Tick(time.Second) {
select {
case <-ctx.Done():
fmt.Println("我要閉嘴了")
return
default:
fmt.Println("balabalabalabala")
}
}
}
timerCtx
timerCtx是一種基于cancelCtx的context類型,從字面上就能看出,這是一種可以定時取消的context。
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) {
//將內(nèi)部的cancelCtx取消
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
取消計時器
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
timerCtx內(nèi)部使用cancelCtx實現(xiàn)取消,另外使用定時器timer和過期時間deadline實現(xiàn)定時取消的功能。timerCtx在調(diào)用cancel方法,會先將內(nèi)部的cancelCtx取消,如果需要則將自己從cancelCtx祖先節(jié)點上移除,最后取消計時器。
WithDeadline
WithDeadline 用于設(shè)置一個絕對時間,表示在某個具體的時間點超時,例如 context.WithDeadline(parentContext, time.Now().Add(10 * time.Second)) 表示在當(dāng)前時間的 10 秒后超時。
package main
import (
"context"
"fmt"
"time"
)
func main() {
HttpHandler()
}
func NewContextWithTimeout() (context.Context, context.CancelFunc) {
return context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
}
func HttpHandler() {
ctx, cancel := NewContextWithTimeout()
defer cancel()
deal(ctx)
}
func deal(ctx context.Context) {
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
}
}
}
WithTimeout
WithTimeout 用于設(shè)置一個相對時間,表示在多長時間后超時,例如 context.WithTimeout(parentContext, 5 * time.Second) 表示在 5 秒后超時。
package main
import (
"context"
"fmt"
"time"
)
func main() {
HttpHandler()
}
func NewContextWithTimeout() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 3*time.Second)
}
func HttpHandler() {
ctx, cancel := NewContextWithTimeout()
defer cancel()
deal(ctx)
}
func deal(ctx context.Context) {
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
}
}
}總結(jié)
context主要用于父子任務(wù)之間的同步取消信號,本質(zhì)上是一種協(xié)程調(diào)度的方式。另外在使用context時有兩點值得注意:上游任務(wù)僅僅使用context通知下游任務(wù)不再需要,但不會直接干涉和中斷下游任務(wù)的執(zhí)行,由下游任務(wù)自行決定后續(xù)的處理操作,也就是說context的取消操作是無侵入的;context是線程安全的,因為context本身是不可變的(immutable),因此可以放心地在多個協(xié)程中傳遞使用。
到此這篇關(guān)于GO語言Context的作用及各種使用方法的文章就介紹到這了,更多相關(guān)GO語言Context使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言模型:string的底層數(shù)據(jù)結(jié)構(gòu)與高效操作詳解
這篇文章主要介紹了Go語言模型:string的底層數(shù)據(jù)結(jié)構(gòu)與高效操作詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
golang多次讀取http request body的問題分析
這篇文章主要給大家分析了golang多次讀取http request body的問題,文中通過代碼示例和圖文介紹的非常詳細,對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-01-01
Go pprof內(nèi)存指標(biāo)含義備忘錄及案例分析
這篇文章主要介紹了Go pprof內(nèi)存指標(biāo)含義備忘錄問題,小編特此把問題及案例分享到腳本之家平臺供大家學(xué)習(xí),需要的朋友可以參考下2020-03-03

