context
context يُترجم كسياق، والغرض من تصميمه هو تمرير الإشارات وبعض البيانات البسيطة عبر عدة كوروتينات خاصة بين الكوروتينات الآباء والأبناء. يُستخدم عادةً في معالجة طلبات HTTP، وجدولة المهام، واستعلامات قواعد البيانات، وغيرها من السيناريوهات، خاصة في هندسة الخدمات المصغرة، حيث يستخدم gRPC context لتمرير البيانات الوصفية والتحكم في السلسلة عبر العمليات والشبكات.
package main
import (
"context"
"fmt"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
defer stop()
for {
select {
case <-ctx.Done():
fmt.Println("terminate")
return
default:
}
fmt.Println("running")
time.Sleep(100 * time.Millisecond)
}
}في المثال أعلاه، يُستخدم context لتمرير إشارات العملية، عند استقبال إشارة، يخرج البرنامج تلقائيًا، وهذا أحد سيناريوهات تطبيق context.
البنية
context.context ليس تنفيذًا محددًا، بل واجهة تُعرّف مجموعة من الطرق
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Deadline()، تُعيد تاريخ انتهاء وقيمة منطقية تشير هل انتهى أم لاDone()، تُعيد قناة لإعلام رسالة الانتهاءErr()، تُعيد سبب إغلاق السياقValue()، تحصل على قيمة محددة حسب المفتاح
في المكتبة القياسية توجد عدة سياقات متاحة:
Background، سياق فارغ، يُستخدم عادةً كعقدة جذر للسياقWithCancel،WithCancelCause، سياق قابل للإلغاءWithDeadline،WithDeadlineCause، سياق مع وقت انتهاءWithTimeout،WithTimeoutCause، سياق مع مهلة زمنيةWithValue، سياق يمكنه تمرير القيم
أما من ناحية التنفيذ فهو بشكل رئيسي:
timerCtxcancelCtxemptyCtx
إذن وظائفه الأساسية أربع نقاط:
- الإلغاء
- الانتهاء
- تمرير القيم
- الانتشار
فهم هذه النقاط يعني فهم مبدأ عمل context.
الإلغاء
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of 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
cause error // set to non-nil by the first cancel call
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}جوهر cancelCtx يكمن في طريقة propagateCancel، وهي مسؤولة عن نشر سلوك الإلغاء بين السياقات الآباء والأبناء.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
...
}أولًا تستدعي للتحقق هل يمكن إلغاء السياق الأب، إذا لم يمكن تعود مباشرة
godone := parent.Done() if done == nil { return // parent is never canceled }ثم تتحقق هل السياق الأب قد أُلغي بالفعل، إذا كان كذلك تُلغي جميع السياقات الأبناء
goselect { case <-done: // parent is already canceled child.cancel(false, parent.Err(), Cause(parent)) return default: }تحاول تحويل السياق الأب لنوع
cancelCtx، إذا نجحت تضيف السياق الحالي لـchildrenفي السياق الأب.goif p, ok := parentCancelCtx(parent); ok { // parent is a *cancelCtx, or derives from one. p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err, p.cause) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() return }تحاول تحويله لنوع
afterFuncer، إذا نجحت تسجل طريقة إلغاء السياق الحالي فيAfterFuncللسياق الأبgoif a, ok := parent.(afterFuncer); ok { // parent implements an AfterFunc method. c.mu.Lock() stop := a.AfterFunc(func() { child.cancel(false, parent.Err(), Cause(parent)) }) c.Context = stopCtx{ Context: parent, stop: stop, } c.mu.Unlock() return }إذا لم ينجح أي مما سبق، تفتح كوروتين منفصل لمراقبة قناة
Done، عند استقبال إشارة تُلغي السياق الابنgogo func() { select { case <-parent.Done(): child.cancel(false, parent.Err(), Cause(parent)) case <-child.Done(): } }()
ثم طريقة cancelCtx.cancel هي المسؤولة نهائيًا عن إلغاء السياق الابن
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
...
}عمليتها كالتالي:
التحقق هل السياق الحالي قد أُلغي
goif err == nil { panic("context: internal error: missing cancel error") } if cause == nil { cause = err } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled }إغلاق قناة
done، إرسال إشعار الإغلاقgoc.err = err c.cause = cause d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) }عبور وإعلام السياقات الأبناء
gofor child := range c.children { child.cancel(false, err, cause) } c.children = nil c.mu.Unlock()أخيرًا حسب المعامل المُمرر تُقرر هل تحتاج للحذف من السياق الأب
if removeFromParent { removeChild(c.Context, c) }
الانتهاء
WithTimeout و WithDeadline كلاهما سياقان مع وقت انتهاء، كلاهما من نفس النوع، فقط اختلاف في دلالة الاستخدام، وكلاهما مبني على cancelCtx
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}WithDeadlineCause مسؤولة عن إنشاء سياق مع وقت انتهاء
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
...
}عمليتها كالتالي:
فحص وقت الانتهاء، إذا كان وقت انتهاء الأب أسبق من الوقت الحالي، فالأب سيلغى قبل السياق الحالي، لذا تُنشئ سياقًا من نوع
cancelCtxمباشرةgoif parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) }بناء
timeCtx، ونشره للسياق الابنgoc := &timerCtx{ deadline: d, } c.cancelCtx.propagateCancel(parent, c)حساب وقت الانتهاء الحالي، إذا انتهى بالفعل تُلغي مباشرة
godur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, cause) // deadline has already passed return c, func() { c.cancel(false, Canceled, nil) } }إذا لم ينتهِ، عبر
time.AfterFuncتضبط إلغاء السياق الحالي عند وقت الانتهاءgoc.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded, cause) }) } return c, func() { c.cancel(true, Canceled, nil) }
بالنسبة لـ timerCtx، طريقة إلغائه فقط توقف timer، ثم توقف السياق الابن أيضًا
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
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()
}تمرير القيم
valueCtx يمكنه تمرير واسترجاع القيم في السياق
type valueCtx struct {
Context
key, val any
}
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}جوهره يكمن في دالة value، وباقي تطبيقات السياق تدخل هذه الدالة للحصول على القيم، مثلًا
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}دالة value هي حلقة for كبيرة، تبحث عن قيمة المفتاح المحدد عبر التكرار الصاعد
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}معالجة أنواع Context المختلفة:
*valueCtx: إذا طابقkeyمفتاحContextالحالي، تُعيدvalالمقابل للمفتاح.*cancelCtx: إذا طابقkeyقيمةcancelCtxKey، تُعيدcancelCtxالحالي، أي تُعيد نفسها.withoutCancelCtx: تعنيContextبدون وظيفة إلغاء، إذا طابقkeyقيمةcancelCtxKey، تُعيدnil.*timerCtx: إذا طابقkeyقيمةcancelCtxKey، تُعيدcancelCtxالمرتبط به.backgroundCtxوtodoCtx: عادةً أنواع خاصة منContextلا تحمل أي قيم إضافية، عند مواجهة هذين النوعين تعيدnilمباشرة.- إذا كان نوعًا غير معروف، تستمر في استدعاء طريقة
Valueللبحث.
ملخص
جوهر هذه السياقات هو cancelCtx، من خلاله تُنشر إشارات الإلغاء، وباقي أنواع السياق وبعض أنواع السياق الخارجية كلها طبقة فوق طبقة، وعلى هذا الأساس تُنفذ وظائف متنوعة.
