
تعد حزمة السياق في Go مفيدة للتفاعلات مع واجهات برمجة التطبيقات والعمليات البطيئة ، خاصة في أنظمة تصنيف الإنتاج التي تتعامل مع طلبات الويب. بمساعدتها ، يمكن إخطار goroutines بالحاجة إلى إكمال عملهم.
يوجد أدناه دليل صغير لمساعدتك في استخدام هذه الحزمة في مشاريعك ، وكذلك بعض أفضل الممارسات والمخاطر.
(ملاحظة: يتم استخدام السياق في العديد من الحزم ، على سبيل المثال ، في العمل مع Docker ).
قبل أن تبدأ
لاستخدام السياقات ، يجب أن تفهم ما هي goroutine والقنوات. سأحاول النظر فيها لفترة وجيزة. إذا كنت معتادًا عليها بالفعل ، فانتقل مباشرةً إلى قسم السياق.
Gorutina
تقول الوثائق الرسمية أن "Gorutin هو تيار خفيف الوزن من التنفيذ". Goroutines أخف وزنا من المواضيع ، لذلك إدارتها أقل نسبيا من الموارد الكثيفة.
→ الصندوق الرمل
package main import "fmt" // , Hello func printHello() { fmt.Println("Hello from printHello") } func main() { // // go func(){fmt.Println("Hello inline")}() // go printHello() fmt.Println("Hello from main") }
إذا قمت بتشغيل هذا البرنامج ، فسترى أنه Hello from main
طباعة " Hello from main
فقط. في الواقع ، تبدأ كل من goroutines ، ولكن التشطيبات main
وقت سابق. لذا ، فإن Goroutines بحاجة إلى وسيلة لإبلاغ main
حول نهاية إعدامهم ، وحتى تنتظر هذا. هنا تأتي القنوات لمساعدتنا.
القنوات (قنوات)
القنوات هي وسيلة للتواصل بين goroutines. يتم استخدامها عندما تريد نقل النتائج أو الأخطاء أو غيرها من المعلومات من goroutine إلى آخر. القنوات من أنواع مختلفة ، على سبيل المثال ، قناة من النوع int
تستقبل أعداد صحيحة ، وقناة أنواع error
تستقبل الأخطاء ، إلخ.
قل لدينا قناة ch
من النوع int
. إذا كنت ترغب في إرسال شيء ما إلى القناة ، فسيكون بناء الجملة ch <- 1
. يمكنك الحصول على شيء من القناة مثل هذا: var := <- ch
، أي خذ القيمة من القناة واحفظها في متغير var
.
توضح التعليمة البرمجية التالية كيفية استخدام القنوات لتأكيد أن goroutines قد أكملت عملها وأرجعت قيمها إلى main
.
ملاحظة: يمكن أيضًا استخدام مجموعات الانتظار للمزامنة ، لكنني في هذه المقالة حددت قنوات لأمثلة الكود ، حيث أننا سنستخدمها لاحقًا في قسم السياق.
→ الصندوق الرمل
package main import "fmt" // int func printHello(ch chan int) { fmt.Println("Hello from printHello") // ch <- 2 } func main() { // . make // : // ch := make(chan int, 2), . ch := make(chan int) // . , . // go func(){ fmt.Println("Hello inline") // ch <- 1 }() // go printHello(ch) fmt.Println("Hello from main") // // , i := <- ch fmt.Println("Received ",i) // // , <- ch }
في السياق (سياق)
تسمح لك حزمة السياق قيد التنفيذ بتمرير البيانات إلى برنامجك في نوع من "السياق". يشير السياق ، مثل المهلة أو الموعد النهائي أو القناة ، إلى إيقاف التشغيل وإعادة المكالمات.
على سبيل المثال ، إذا قمت بتقديم طلب ويب أو قمت بتنفيذ أمر نظام ، فمن الجيد استخدام مهلة لأنظمة تقدير الإنتاج. لأنه إذا كانت واجهة برمجة التطبيقات التي تقوم بالوصول إليها بطيئة ، فمن غير المرجح أن ترغب في تجميع الطلبات في نظامك ، لأن هذا قد يؤدي إلى زيادة الحمل وانخفاض الأداء عند معالجة طلباتك الخاصة. والنتيجة هي تأثير تتالي.
وهنا قد يكون سياق المهلة أو الموعد النهائي صحيحًا تمامًا.
إنشاء السياق
تسمح لك حزمة السياق بإنشاء السياق ورثه بالطرق التالية:
سياق. خلفية () ctx السياق
هذه الدالة تقوم بإرجاع سياق فارغ. يجب استخدامه فقط على مستوى عالٍ (بشكل رئيسي أو في معالج طلب أعلى مستوى). يمكن استخدامه للحصول على سياقات أخرى ، والتي سنناقشها لاحقًا.
ctx, cancel := context.Background()
تقريبا. عبر: هناك عدم دقة في المقالة الأصلية ، والمثال الصحيح لاستخدام context.Background
سيكون على النحو التالي:
ctx := context.Background()
سياق. TODO () ctx السياق
تعمل هذه الوظيفة أيضًا على إنشاء سياق فارغ. ويجب أيضًا استخدامه فقط على مستوى عالٍ ، إما عندما تكون غير متأكد من السياق الذي يجب استخدامه ، أو إذا لم تتلق الوظيفة بعد السياق المطلوب. هذا يعني أنك (أو أي شخص يدعم الكود) تخطط لإضافة سياق إلى الوظيفة لاحقًا.
ctx, cancel := context.TODO()
تقريبا. عبر: هناك عدم دقة في المقال الأصلي ، والمثال الصحيح لاستخدام context.TODO
سيكون على النحو التالي:
ctx := context.TODO()
ومن المثير للاهتمام أن نلقي نظرة على الكود ، إنه نفس الخلفية تمامًا. يتمثل الاختلاف الوحيد في أنه في هذه الحالة ، يمكنك استخدام أدوات التحليل الثابتة للتحقق من صحة نقل السياق ، والذي يعد تفصيلًا مهمًا ، لأن هذه الأدوات تساعد في تحديد الأخطاء المحتملة في مرحلة مبكرة ويمكن تضمينها في خط أنابيب CI / CD.
من هنا :
var ( background = new(emptyCtx) todo = new(emptyCtx) )
context.WithValue (السياق الأصلي ، المفتاح ، واجهة val {}) (سياق ctx ، إلغاء CancelFunc)
تقريبا. حارة: هناك عدم دقة في المقالة الأصلية ، والتوقيع الصحيح context.WithValue
ستكون على النحو التالي:
context.WithValue(parent Context, key, val interface{}) Context
تأخذ هذه الوظيفة سياقًا وتقوم بإرجاع سياق مشتق منه ترتبط فيه قيمة val
key
ويمر عبر شجرة السياق بأكملها. أي بمجرد إنشاء سياق WithValue
، WithValue
أي سياق مشتق هذه القيمة.
لا ينصح بتمرير المعلمات الحرجة باستخدام قيم السياق ؛ بدلاً من ذلك ، يجب أن تأخذها الدوال بوضوح في التوقيع.
ctx := context.WithValue(context.Background(), key, "test")
سياق .WithCancel (الأصل السياق) (سياق CTX ، إلغاء CancelFunc)
يصبح أكثر إثارة للاهتمام هنا قليلا. تقوم هذه الوظيفة بإنشاء سياق جديد من الأصل الذي تم تمريره إليها. يمكن أن يكون الأصل هو سياق الخلفية أو يتم تمرير السياق كوسيطة للدالة.
يتم إرجاع السياق المشتق ووظيفة التراجع. فقط الوظيفة التي تنشئها يجب أن تستدعي الوظيفة لإلغاء السياق. يمكنك تمرير وظيفة التراجع إلى وظائف أخرى إذا كنت ترغب في ذلك ، ولكن هذا لا يشجعك بشدة. عادة ما يتم اتخاذ هذا القرار من سوء فهم لإلغاء السياق. لهذا السبب ، يمكن أن تؤثر السياقات الناتجة عن هذا الأصل على البرنامج ، مما سيؤدي إلى نتيجة غير متوقعة. باختصار ، من الأفضل عدم اجتياز وظيفة إلغاء.
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
تقريبا. حارة: في المقال الأصلي ، المؤلف ، على ما يبدو ، عن طريق الخطأ context.WithCancel
قدم مثالا مع context.WithDeadline
. المثال الصحيح context.WithCancel
ctx, cancel := context.WithCancel(context.Background())
سياق .WithDeadline (الأصل السياق ، د الوقت. الوقت) (سياق CTX ، إلغاء CancelFunc)
تُرجع هذه الوظيفة سياقًا مشتقًا من أصلها ، والذي يتم إلغاؤه بعد الموعد النهائي أو استدعاء وظيفة الإلغاء. على سبيل المثال ، يمكنك إنشاء سياق يتم إلغاؤه تلقائيًا في وقت محدد وينقل هذا إلى وظائف تابعة. عندما يتم إلغاء هذا السياق بعد الموعد النهائي ، يجب إخطار جميع الوظائف التي تحتوي على هذا السياق عن طريق الإخطار.
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
context.WithTimeout (السياق الأصلي ، وقت المهلة. المدة) (سياق ctx ، إلغاء CancelFunc)
هذه الوظيفة مشابهة للسياق. الفرق هو أن طول الوقت يستخدم كمدخلات. هذه الدالة تقوم بإرجاع سياق مشتق يتم إلغاؤه عندما يتم استدعاء وظيفة الإلغاء أو بعد وقت.
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
تقريبا. حارة: في المقال الأصلي ، قدم المؤلف ، على ما يبدو ، خطأً context.WithTimeout
أعطى مثال مع context.WithDeadline
. مثال: context.WithDeadline
. المثال الصحيح لـ context.WithTimeout
سيكون هذا:
ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
استقبال واستخدام السياقات في وظائفك
الآن بعد أن عرفنا كيفية إنشاء سياقات (الخلفية و TODO) وكيفية إنشاء سياقات (WithValue و WithCancel و الموعد النهائي و Timeout) ، دعونا نناقش كيفية استخدامها.
في المثال التالي ، يمكنك أن ترى أن الوظيفة التي تأخذ السياق تطلق goroutine وتتوقع أن تعود أو تلغي السياق. تساعد عبارة التحديد في تحديد ما يحدث أولاً وإنهاء الوظيفة.
بعد إغلاق القناة تم <-ctx.Done()
، يتم تحديد حالة case <-ctx.Done():
بمجرد حدوث ذلك ، يجب أن تقاطع الوظيفة العمل وتستعد للعودة. هذا يعني أنه يجب عليك إغلاق أي اتصالات مفتوحة وتحرير الموارد والعودة من الوظيفة. في بعض الأحيان ، قد يؤدي تأخير الموارد إلى تأخير الإرجاع ، على سبيل المثال ، توقف التنظيف. يجب أن تضع ذلك في الاعتبار.
المثال الذي يتبع هذا القسم هو برنامج go انتهى بالكامل يوضح مهلات التراجع ووظائف التراجع.
// , - // , - func sleepRandomContext(ctx context.Context, ch chan bool) { // (. .: ) // // , defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() // sleeptimeChan := make(chan int) // // go sleepRandom("sleepRandomContext", sleeptimeChan) // select select { case <-ctx.Done(): // , // , - // , ( ) // - , // , // fmt.Println("Time to return") case sleeptime := <-sleeptimeChan: // , fmt.Println("Slept for ", sleeptime, "ms") } }
مثال
كما رأينا ، باستخدام السياقات ، يمكنك العمل مع المواعيد النهائية ، والمهلة الزمنية ، وكذلك استدعاء وظيفة الإلغاء ، مما يجعل من الواضح لجميع الوظائف باستخدام سياق مشتق أنك بحاجة إلى إكمال عملك وتنفيذ الإرجاع. النظر في مثال:
الوظيفة main
:
- يخلق سياق وظيفة الغاء
- يستدعي وظيفة الإلغاء بعد انتهاء مهلة تعسفية
وظيفة doWorkContext
:
- يخلق سياق مشتق مع مهلة
- يتم إلغاء هذا السياق عندما تقوم الوظيفة الرئيسية باستدعاء deleteFunction ، أو انتهاء المهلة ، أو باستدعاء doWorkContext إلغاء دالة.
- يدير goroutine لأداء بعض المهام البطيئة ، ويمر السياق الناتج
- ينتظر لإكمال goroutines أو يتم إلغاء السياق من الرئيسي ، أيهما يأتي أولاً
sleepRandomContext
وظيفة:
- تطلق goroutine لأداء بعض المهام البطيئة
- ينتظر goroutine حتى النهاية ، أو
- ينتظر أن يتم إلغاء السياق من خلال الوظيفة الرئيسية أو المهلة أو الاتصال بإلغاء الدالة الخاصة به
sleepRandom
:
يستخدم هذا المثال وضع السكون لمحاكاة وقت المعالجة العشوائي ، لكن في الواقع ، يمكنك استخدام القنوات للإشارة إلى هذه الوظيفة عن بدء التنظيف وانتظار التأكيد من القناة على اكتمال التنظيف.
Sandbox (يبدو أن الوقت العشوائي الذي أستخدمه في Sandbox لم يتغير عملياً. جرب هذا على الكمبيوتر المحلي الخاص بك لرؤية العشوائية)
→ جيثب
package main import ( "context" "fmt" "math/rand" "Time" ) // func sleepRandom(fromFunction string, ch chan int) { // defer func() { fmt.Println(fromFunction, "sleepRandom complete") }() // // , // «» seed := time.Now().UnixNano() r := rand.New(rand.NewSource(seed)) randomNumber := r.Intn(100) sleeptime := randomNumber + 100 fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms") time.Sleep(time.Duration(sleeptime) * time.Millisecond) fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms") // , if ch != nil { ch <- sleeptime } } // , // , - func sleepRandomContext(ctx context.Context, ch chan bool) { // (. .: ) // // , defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() // sleeptimeChan := make(chan int) // // go sleepRandom("sleepRandomContext", sleeptimeChan) // select select { case <-ctx.Done(): // , // , doWorkContext // doWorkContext main cancelFunction // , - // , ( ) // - , // , // fmt.Println("sleepRandomContext: Time to return") case sleeptime := <-sleeptimeChan: // , fmt.Println("Slept for ", sleeptime, "ms") } } // , // // , main func doWorkContext(ctx context.Context) { // - // 150 // , , 150 ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond) // defer func() { fmt.Println("doWorkContext complete") cancelFunction() }() // // , // , ch := make(chan bool) go sleepRandomContext(ctxWithTimeout, ch) // select select { case <-ctx.Done(): // , // , main cancelFunction fmt.Println("doWorkContext: Time to return") case <-ch: // , fmt.Println("sleepRandomContext returned") } } func main() { // background ctx := context.Background() // ctxWithCancel, cancelFunction := context.WithCancel(ctx) // // defer func() { fmt.Println("Main Defer: canceling context") cancelFunction() }() // - // , go func() { sleepRandom("Main", nil) cancelFunction() fmt.Println("Main Sleep complete. canceling context") }() // doWorkContext(ctxWithCancel) }
المزالق
إذا كانت الوظيفة تستخدم السياق ، فتأكد من معالجة إشعارات الإلغاء بشكل صحيح. على سبيل المثال ، لا exec.CommandContext
ذلك exec.CommandContext
قناة القراءة حتى يكمل الأمر جميع الشوكات التي تم إنشاؤها بواسطة العملية ( Github ) ، أي أن إلغاء السياق لا يُرجع فورًا من الوظيفة إذا انتظرت cmd.Wait () ، حتى جميع الشوك من القيادة الخارجية كاملة المعالجة.
إذا كنت تستخدم مهلة أو الموعد النهائي مع الحد الأقصى لوقت التشغيل ، فقد لا يعمل كما هو متوقع. في مثل هذه الحالات ، من الأفضل تنفيذ المهلات باستخدام time.After
.
أفضل الممارسات
- context.Background يجب استخدامه فقط على أعلى مستوى ، كجذر لجميع السياقات المشتقة.
- سياق. يجب استخدام TODO عندما لا تكون متأكدًا مما يجب استخدامه ، أو إذا كانت الوظيفة الحالية ستستخدم السياق في المستقبل.
- يوصى بإلغاء السياق ، ولكن قد تستغرق هذه الوظائف بعض الوقت لمسحها والخروج منها.
- context.Value يجب أن تستخدم بشكل ضئيل قدر الإمكان ، ويجب ألا تستخدم لتمرير المعلمات الاختيارية. هذا يجعل API غير مفهومة ويمكن أن يؤدي إلى أخطاء. يجب أن يتم تمرير هذه القيم كوسائط.
- لا تقم بتخزين السياقات في بنية ؛ قم بتمريرها بوضوح في وظائف ، ويفضل أن تكون الوسيطة الأولى.
- لا تمر أبدًا في سياق عدم وجود حجة. إذا كنت في شك ، استخدم TODO.
- لا تحتوي بنية
Context
على طريقة إلغاء ، لأن الوظيفة التي تولد السياق فقط هي التي يجب أن تلغيها.
من المترجم
في شركتنا ، نستخدم حزمة Context بنشاط عند تطوير تطبيقات الخادم للاستخدام الداخلي. لكن هذه التطبيقات للأداء الطبيعي ، بالإضافة إلى السياق ، تتطلب عناصر إضافية ، مثل:
- تسجيل
- معالجة الإشارات لإنهاء التطبيق ، إعادة تحميل و logrotate
- العمل مع ملفات pid
- العمل مع ملفات التكوين
- و غيرها
لذلك ، في مرحلة ما ، قررنا تلخيص كل خبرتنا وإنشاء حزم إضافية تبسط تطبيقات الكتابة إلى حد كبير (خاصة التطبيقات التي تحتوي على واجهات برمجة التطبيقات). لقد نشرنا تطوراتنا في المجال العام ويمكن لأي شخص استخدامها. فيما يلي بعض الارتباطات إلى الحزم المفيدة لحل هذه المشكلات:
اقرأ أيضًا مقالات أخرى على مدونتنا: