كيفية استخدام سياق السياق بشكل صحيح

سيتحدث هذا المنشور عن مكتبة جديدة في Go 1.7 ، ومكتبة السياق ، ومتى أو كيفية استخدامها بشكل صحيح. القراءة المطلوبة للبدء هي المنشور التمهيدي الذي يتحدث قليلاً عن المكتبة وبشكل عام عن كيفية استخدامها. يمكنك قراءة وثائق مكتبة السياق على tip.golang.org.

كيفية دمج السياق في API الخاص بك

أهم شيء يجب تذكره عند دمج Context في API الخاص بك هو أن الغرض منه هو طلب النطاق. على سبيل المثال ، قد يكون من المنطقي التواجد في استعلام قاعدة بيانات واحدة ، لكن لن يكون من المنطقي التواجد في كائن قاعدة بيانات.

يوجد حاليًا طريقتان لدمج كائنات السياق في واجهة برمجة التطبيقات:

  • المعلمة الأولى لاستدعاء وظيفة
  • التكوين اختياري على هيكل الطلب

للحصول على مثال عن الأول ، انظر Dialer.DialContext الخاص بحزمة net. تقوم هذه الوظيفة بعملية Dial عادية ، ولكنها تلغيها وفقًا لكائن Context.

func (d * Dialer) DialContext (سياق ctx. السياق ، الشبكة ، سلسلة العناوين) (Conn ، خطأ)

للحصول على مثال عن الطريقة الثانية لدمج السياق ، راجع طلب net / http الخاص بـ package.WithContext

func (r * Request) WithContext (ctx context.Context) * Request

يؤدي هذا إلى إنشاء كائن طلب جديد ينتهي وفقًا للسياق المحدد.

يجب أن يتدفق السياق من خلال البرنامج

نموذج عقلي رائع لاستخدام السياق هو أنه يجب أن يتدفق عبر البرنامج. تخيل النهر أو المياه الجارية. يعني هذا عمومًا أنك لا تريد تخزينه في مكان ما كما هو الحال في إحدى المنشآت. ولا تريد الاحتفاظ بها في أي مكان أكثر مما هو مطلوب بشدة. يجب أن يكون السياق عبارة عن واجهة يتم تمريرها من وظيفة إلى أسفل مكدس الاتصال الخاص بك ، زيادة حسب الحاجة. من الناحية المثالية ، يتم إنشاء كائن السياق مع كل طلب وينتهي عند انتهاء الطلب.

الاستثناء الوحيد لعدم تخزين سياق هو عندما تحتاج إلى وضعه في بنية يتم استخدامها فقط كرسالة يتم تمريرها عبر قناة. هذا يظهر في المثال أدناه.

في هذا المثال ، نخرق هذه القاعدة العامة لعدم تخزين السياق عن طريق وضعه في رسالة. ومع ذلك ، يعد هذا استخدامًا مناسبًا للسياق لأنه لا يزال يتدفق عبر البرنامج ، ولكن عبر قناة بدلاً من تتبع مكدس. لاحظ أيضًا كيف يتم استخدام السياق في أربعة أماكن:

  • إلى مهلة q في حالة امتلاء المعالج
  • لإعلام q إذا كان يجب معالجة الرسالة
  • إلى مهلة q إرسال الرسالة مرة أخرى إلى newRequest ()
  • إلى مهلة newRequest () في انتظار استجابة مرة أخرى من ProcessMessage

يجب إلغاء جميع عمليات الحظر / العمليات الطويلة

عندما تستبعد قدرة المستخدمين على إلغاء العمليات المستمرة منذ فترة طويلة ، فأنت تربط مجموعة goroutine أطول مما يريد المستخدم. مع انتقال السياق إلى المكتبة القياسية باستخدام Go 1.7 ، سيصبح بسهولة التجريد القياسي للتوقيت أو إنهاء عمليات التشغيل الطويلة المبكرة. إذا كنت تكتب مكتبة وقد تمنع وظائفك ، فهي حالة استخدام مثالية للسياق.

في المثال أعلاه ، تعتبر ProcessMessage عملية سريعة لا تمنعها وبالتالي من الواضح أن السياق مبالغ فيه. ومع ذلك ، إذا كانت العملية أطول بكثير ، فإن استخدام Context من قِبل المتصل يتيح لـ newRequest المضي قدمًا إذا استغرق الأمر وقتًا طويلاً للغاية في الحساب.

السياق. القيم والقيم المحددة بالطلب (تحذير)

الجزء الأكثر إثارة للجدل من السياق هو القيمة ، والتي تسمح بالقيم التعسفية الموضوعة في السياق. الاستخدام المقصود لـ Context.Value ، من منشور المدونة الأصلي ، هو قيم تم تحديدها حسب الطلب. قيمة النطاق التي تم تحديدها هي قيمة مشتقة من البيانات الموجودة في الطلب الوارد وتنتهي عندما ينتهي الطلب. عندما يرتد الطلب بين الخدمات ، يتم الاحتفاظ بهذه البيانات غالبًا بين مكالمات RPC. دعنا أولاً نحاول توضيح ما هو أو ليس قيمة نطاق طلب.

يمكن أن يكون الطلب الواضح الذي تم تحديد نطاقه هو من يقوم بتقديم الطلب (معرف المستخدم) ، وكيف يتم إعداده (داخليًا أو خارجيًا) ، ومن حيث يقومون بعمله (IP user) ، وما مدى أهمية هذا الطلب.

لا يعتبر اتصال قاعدة البيانات قيمة نطاق طلب لأنه عالمي بالنسبة للخادم بالكامل. من ناحية أخرى ، إذا كان اتصالًا يحتوي على بيانات تعريف حول المستخدم الحالي لتعبئة الحقول تلقائيًا مثل معرف المستخدم أو مصادقة القيام به ، فيمكن اعتبار الطلب تم تحديد نطاقه.

لا يتم طلب تسجيل النطاق إذا كان موجودًا على كائن خادم أو كان منفردًا من الحزمة. ومع ذلك ، إذا كان يحتوي على بيانات تعريف حول من قام بإرسال الطلب وربما إذا كان الطلب قد تم تمكين تسجيل تصحيح الأخطاء ، فسيصبح الطلب محددًا.

لسوء الحظ ، يمكن أن يشتمل طلب البيانات التي تم تحديد نطاقها على مجموعة كبيرة من المعلومات نظرًا لأن بعض البيانات المهمة في التطبيق تأتي من طلب ما. يضع هذا تعريفًا واسعًا لما يمكن تضمينه في Context.Value ، مما يجعل من السهل إساءة استخدامه. أنا شخصياً لدي رؤية أضيق لما هو مناسب في السياق. القيمة وسأحاول شرح موقفي في بقية هذا المنشور.

Context.Value يحجب تدفق البرنامج

السبب الحقيقي لذلك يتم وضع الكثير من القيود على الاستخدام الصحيح لـ Context.Value هو أنه يحجب المدخلات والمخرجات المتوقعة من وظيفة أو مكتبة. يكره الكثيرون للأسباب نفسها التي يكرهها الناس. معلمات دالة واضحة وتوثيق الاكتفاء الذاتي ما هو مطلوب لجعل هذه الوظيفة تتصرف. هذا يجعل الوظيفة سهلة الاختبار والسبب بشكل حدسي ، وكذلك refactor في وقت لاحق. على سبيل المثال ، ضع في الاعتبار الوظيفة التالية التي تقوم بالتوثيق من السياق.

func IsAdminUser (ctx context.Context) bool {
  x: = token.GetToken (ctx)
  userObject: = auth.AuthenticateToken (x)
  إرجاع userObject.IsAdmin () || userObject.IsRoot ()
}

عندما يتصل المستخدمون بهذه الوظيفة ، فإنهم يرون فقط أنها تتطلب سياقًا. ولكن الأجزاء المطلوبة لمعرفة ما إذا كان المستخدم هو المسؤول هي بوضوح شيئين: خدمة المصادقة (في هذه الحالة تستخدم كالتالي المفرد) ورمز المصادقة. يمكنك تمثيل ذلك كمدخلات ومخرجات مثل أدناه.

تدفق IsAdminUser

دعونا نمثل هذا التدفق بوضوح بوظيفة ، مع إزالة كل الحروف المفردة والسياقات.

FunAd IsAdminUser (سلسلة الرمز المميز ، authService AuthService) int {
  userObject: = authService.AuthenticateToken (الرمز المميز)
  إرجاع userObject.IsAdmin () || userObject.IsRoot ()
}

يعد تعريف الوظيفة الآن تمثيلًا واضحًا لما يجب معرفته إذا كان المستخدم مسؤولًا. هذا التمثيل واضح أيضًا لمستخدم الوظيفة ويجعل إعادة بناء الوظيفة وإعادة استخدامها أكثر قابلية للفهم.

Context.Value وحقائق النظم الكبيرة

أنا أتعاطف بشدة مع الرغبة في دفع العناصر في Context.Value. غالبًا ما تحتوي الأنظمة المعقدة على طبقات برامج وسيطة وتجريدات متعددة على طول مكدس الاستدعاءات. تعتبر القيم المحسوبة في الجزء العلوي من مكدس الاستدعاء مملة وصعبة وقبيحة للمتصلين إذا كان عليك إضافتهم إلى كل مكالمة دالة بين أعلى وأسفل لنشر شيء بسيط مثل معرف المستخدم أو الرمز المميز للمصادقة. تخيل لو اضطررت إلى إضافة معلمة أخرى تسمى "معرف المستخدم" لعشرات الوظائف بين مكالمتين في عبوتين مختلفتين فقط لإعلام الحزمة Z حول الحزمة التي تم اكتشافها؟ ستبدو واجهة برمجة التطبيقات (API) قبيحة ويصرخ الناس عليك لتصميمها. حسن! لمجرد أنك اتخذت هذا القبح وحجبته داخل السياق. القيمة لا تجعل واجهة برمجة التطبيقات أو التصميم الخاص بك أفضل. الغموض هو عكس تصميم API الجيد.

يجب أن تبلغ القيمة ، وليس السيطرة

إعلام ، وليس السيطرة. هذا هو المانترا الأساسي الذي أشعر أنه يجب أن يرشده إذا كنت تستخدم السياق. محتوى context.Value مخصص للمشرفين وليس المستخدمين. لا ينبغي أبدا أن تكون هناك حاجة إلى إدخال النتائج الموثقة أو المتوقعة.

للتوضيح ، إذا كانت وظيفتك لا يمكن أن تتصرف بشكل صحيح بسبب وجود قيمة قد تكون أو لا تكون داخل السياق. القيمة ، فإن API الخاصة بك تحجب المدخلات المطلوبة بشدة. إلى جانب الوثائق ، هناك أيضًا السلوك المتوقع للتطبيق الخاص بك. إذا كانت الوظيفة ، على سبيل المثال ، تتصرف على النحو الموثق ولكن الطريقة التي يستخدم بها التطبيق الخاص بك الوظيفة لها سلوك عملي وهو بحاجة إلى شيء ما في السياق لكي يتصرف بشكل صحيح ، ثم يقترب من التأثير على التحكم في البرنامج.

مثال واحد من المعلومات هو معرف الطلب. بشكل عام ، يتم استخدامها في أنظمة التسجيل أو أنظمة التجميع الأخرى لتجميع الطلبات معًا. لا تغير المحتويات الفعلية لمعرف الطلب نتيجة عبارة if وإذا كان معرف الطلب مفقودًا ، فلن يقوم بأي شيء بتعديل نتيجة دالة ما.

مثال آخر يناسب تعريف inform هو المسجل. وجود أو عدم وجود المسجل لا يغير أبدا تدفق البرنامج. أيضًا ، ما يتم تسجيله أو عدم تسجيله عادةً لا يتم توثيقه أو الاعتماد عليه في معظم الاستخدامات. ومع ذلك ، إذا تم توثيق وجود التسجيل أو محتويات السجل في واجهة برمجة التطبيقات ، فقد انتقل المسجل من الإعلام إلى التحكم.

مثال آخر للإعلام هو عنوان IP للطلب الوارد ، إذا كان الغرض الوحيد من عنوان IP هذا هو تزيين رسائل السجل بعنوان IP الخاص بالمستخدم. ومع ذلك ، إذا كانت الوثائق أو السلوك المتوقع لمكتبتك هي أن بعض عناوين IP هي أكثر أهمية وأقل عرضة للتخثر ، فقد انتقل عنوان IP من المعلومات إلى عنصر التحكم لأنه مطلوب الآن الإدخال أو على الأقل الإدخال الذي يغير السلوك.

يعد اتصال قاعدة البيانات مثالًا أسوأ الحالات على كائن يتم وضعه في سياق. القيمة لأنه يتحكم بوضوح في البرنامج ويطلب إدخال وظائفك.

من المحتمل أن يكون منشور مدونة golang.org على context.Context مثالًا مضادًا لكيفية استخدام context.Value بشكل صحيح. دعونا نلقي نظرة على رمز البحث المنشور في المدونة.

بحث func (ctx context.Context ، سلسلة الاستعلام) (النتائج ، الخطأ) {
 // قم بإعداد طلب Google Search API.
 // ...
 // ...
 س: = req.URL.Query ()
 q.Set ("q" ، استعلام)
/ / إذا كان ctx يحمل عنوان IP الخاص بالمستخدم ، فأرسله إلى الخادم.
 تستخدم واجهات برمجة التطبيقات من Google IP المستخدم لتمييز الطلبات التي يبدأها الخادم
 // من طلبات المستخدم النهائي.
 إذا كان userIP ، موافق: = userip.FromContext (ctx) ؛ حسنا {
   q.Set ("userip" ، userIP.String ())
 }

عصا القياس الأساسية هي معرفة كيفية تغيير وجود userIP في الاستعلام نتيجة طلب ما. إذا تم تمييز عنوان IP في نظام تتبع السجل حتى يتمكن الأشخاص من تصحيح الخادم الوجهة ، فسيبلغهم بحتة وهو موافق. إذا كان userIP الموجود داخل طلب ما يغير سلوك استدعاء REST أو يميل إلى جعله أقل عرضة للتخنق ، فسيبدأ في التحكم في المخرجات المحتملة للبحث ولم يعد مناسبًا لـ Context.Value.

يذكر منشور المدونة أيضًا الرموز المميزة للتخويل كشيء مخزّن في سياق.قيمة. هذا ينتهك بوضوح قواعد المحتوى المناسب في Context.Value لأنه يتحكم في سلوك الوظيفة ويطلب إدخالًا لتدفق البرنامج. بدلاً من ذلك ، من الأفضل جعل الرموز معلمة صريحة أو عضو في بنية.

هل تنتمي Context.Value؟

يقوم السياق بعمل شيئين مختلفين: أحدهما ينقضي مهلة العمليات الطويلة والآخر يحمل قيمًا تم تحديد نطاقها. يجب أن تكون الواجهات في Go حول وصف السلوكيات التي يريدها API. لا ينبغي أن يكونوا أكياس انتزاع من الوظائف التي تحدث في كثير من الأحيان معا. من المؤسف أنني أجبرت على تضمين سلوك حول إضافة قيم تعسفية إلى كائن عندما يكون كل ما يهمني هو إنهاء الطلبات الهاربة.

بدائل Context.Value

غالبًا ما يستخدم الناس Context.Value في تجريد الوسيطة الأوسع. سأعرض هنا كيفية البقاء داخل هذا النوع من التجريد بينما لا أزال بحاجة إلى إساءة استخدام Context.Value. دعنا نعرض بعض الأمثلة على الكود الذي يستخدم HTTP middlewares و Context.Value لنشر معرف مستخدم موجود في بداية البرنامج الوسيط. ملاحظة Go 1.7 يتضمن سياقًا على كائن http.Request. أيضًا ، أنا أفتقد قليلاً مع بناء الجملة ، لكنني آمل أن يكون المعنى واضحًا.

هذا مثال على كيفية استخدام Context.Value غالبًا في سلاسل البرامج الوسيطة لإعداد نشر معرف مستخدم على طول. الوسيطة الأولى ، addUserID ، تقوم بتحديث السياق. ثم يستدعي المعالج التالي في سلسلة الوسيطة. في وقت لاحق يتم استخراج قيمة معرف المستخدم داخل السياق واستخدامها. في التطبيقات الكبيرة ، يمكنك تخيل أن هاتين الوظيفتين بعيدتين جدًا عن بعضهما البعض.

دعنا الآن نوضح كيف يمكننا باستخدام نفس التجريد أن نفعل الشيء نفسه ، لكن لا نحتاج إلى إساءة استخدام Context.Value.

في هذا المثال ، لا يزال بإمكاننا استخدام نفس تجريدات البرامج الوسيطة وما زلنا نعرف الوظيفة الرئيسية فقط عن سلسلة البرامج الوسيطة ، ولكن استخدم معرف المستخدم بطريقة آمنة من النوع. سلسلة chainPartOne المتغيرة هي سلسلة البرامج الوسيطة التي تصل إلى وقت استخراج UserID. يمكن لهذا الجزء من السلسلة إنشاء الجزء التالي من السلسلة ، chainWithAuth ، باستخدام معرف المستخدم مباشرة.

في هذا المثال ، يمكننا الاحتفاظ بالسياق لإنهاء وظائف التشغيل الطويلة المبكرة. لقد وثقنا بوضوح أن الهيكل UseUserID يحتاج إلى معرف مستخدم لكي يتصرف بشكل صحيح. يعني هذا الفصل الواضح أنه عندما يقوم الأشخاص لاحقًا بإعادة تكوين هذا الرمز أو محاولة إعادة استخدام UseUserID ، فإنهم يعرفون ما يمكن توقعه.

لماذا الاستثناء للمشرفين

أعترف باستبعاد استثناء للمشرفين في Context.Value تعسفي إلى حد ما. تفكيري الشخصي هو تخيل نظام مصمم تمامًا. في هذا النظام ، لن تكون هناك حاجة لاستكشاف تطبيق ما ، ولا توجد حاجة لسجلات تصحيح الأخطاء ، ولا توجد حاجة ماسة للقياسات. النظام مثالي حتى لا توجد مشاكل في الصيانة. لسوء الحظ ، الحقيقة هي أننا نحتاج إلى تصحيح الأنظمة. يعد وضع معلومات التصحيح هذه في كائن سياق حل وسط بين واجهة برمجة التطبيقات المثالية التي لن تحتاج أبدًا إلى الصيانة وحقائق الرغبة في ربط معلومات تصحيح الأخطاء عبر واجهة برمجة التطبيقات. ومع ذلك ، فإنني لا أجادل بشكل خاص مع شخص يريد توضيح معلومات تصحيح الأخطاء في واجهة برمجة التطبيقات.

حاول أن لا تستخدم سياق.القيمة

يمكنك الدخول في مشكلة أكثر مما يستحق محاولة استخدام context.Value. أتعاطف مع مدى سهولة إضافة شيء ما إلى السياق. القيمة واسترداده لاحقًا في بعض التجريد البعيد ، ولكن سهولة الاستخدام الآن تدفع مقابل الألم عند إعادة المعالجة في وقت لاحق. لا توجد حاجة لاستخدامها تقريبًا ، وإذا قمت بذلك ، فإن إعادة إنشاء التعليمات البرمجية في وقت لاحق أمر صعب للغاية لأنه يصبح غير معروف (خاصة من قبل المترجم) ما هي المدخلات المطلوبة للوظائف. إنها إضافة مثيرة للجدل إلى Context ويمكن أن تتسبب بسهولة في مشكلة أكثر مما تستحق.