ملخص
تم اقتراح إنشاء try
جديدة تم تصميمها خصيصًا لإزالة if
التعبيرات الشائعة مرتبطة بمعالجة الأخطاء في Go. هذا هو التغيير الوحيد في اللغة. يدعم المؤلفون استخدام وظائف التأجيل والمكتبة القياسية لإثراء أو التفاف الأخطاء. هذا الملحق الصغير مناسب لمعظم السيناريوهات ، عملياً دون تعقيد اللغة.
إن تصميم try
سهل التفسير ، وسهل التنفيذ ، وهذه الوظيفة غير متجانسة مع بنيات اللغات الأخرى ومتوافقة تمامًا مع الإصدارات السابقة. كما أنها قابلة للمد إذا أردنا ذلك في المستقبل.
يتم تنظيم بقية هذا المستند على النحو التالي: بعد مقدمة موجزة ، نقدم تعريفًا للوظيفة المدمجة وشرح استخدامها في الممارسة. يستعرض قسم المناقشة الاقتراحات البديلة والتصميم الحالي. في النهاية ، سيتم تقديم الاستنتاجات وخطة التنفيذ مع أمثلة وقسم من الأسئلة والأجوبة.
مقدمة
في مؤتمر Gophercon الأخير في دنفر ، قدم فريق Go (Russ Cox ، Marcel van Lohuizen) بعض الأفكار الجديدة حول كيفية الحد من التعب اليدوي لمعالجة الأخطاء في Go ( تصميم المسودة ). منذ ذلك الحين تلقينا قدرا كبيرا من ردود الفعل.
كما أوضح Russ Cox في مراجعته للمشكلة ، فإن هدفنا هو جعل معالجة الأخطاء أكثر وزنًا عن طريق تقليل مقدار الشفرة المخصصة بالتحديد للأخطاء. نريد أيضًا أن نجعل رمز معالجة الأخطاء في الكتابة أكثر ملاءمة ، مما يزيد من احتمال أن يواصل المطورون تخصيص وقت لتصحيح الخطأ. في الوقت نفسه ، نريد ترك رمز معالجة الأخطاء مرئيًا بوضوح في رمز البرنامج.
تتركز الأفكار التي تمت مناقشتها في مشروع المسودة حول بيان check
الأحادي الجديد ، والذي يبسط التحقق الصريح من قيمة الخطأ التي تم الحصول عليها من بعض التعبيرات (عادةً ما يكون استدعاء دالة) ، بالإضافة إلى إعلان معالجات الأخطاء ( handle
) ومجموعة من القواعد التي تربط بين هاتين اللغتين الجديدتين.
ركزت معظم الملاحظات التي تلقيناها على تفاصيل وتعقيد تصميم handle
، وفكرة مشغل check
تبين أنها أكثر جاذبية. في الواقع ، أخذ العديد من أعضاء المجتمع فكرة مشغل check
وقاموا بتوسيعه. فيما يلي بعض المشاركات الأكثر تشابهًا مع عرضنا:
الاقتراح الحالي ، على الرغم من اختلافه بالتفصيل ، كان مبني على هذه المقترحات الثلاثة ، وبشكل عام ، على التعليقات الواردة على مشروع التصميم المقترح في العام الماضي.
لإكمال الصورة ، نريد أن نلاحظ أنه يمكن العثور على المزيد من اقتراحات معالجة الأخطاء في صفحة الويكي هذه . تجدر الإشارة أيضًا إلى أن Liam Breck جاءت مع مجموعة واسعة من المتطلبات لآلية معالجة الأخطاء.
أخيرًا ، بعد نشر هذا الاقتراح ، علمنا أن Ryan Hileman قام بتطبيقه قبل خمس سنوات باستخدام أداة og rewriter واستخدمه بنجاح في مشاريع حقيقية. راجع ( https://news.ycombinator.com/item؟id=20101417 ).
المدمج في محاولة وظيفة
عرض
نقترح إضافة عنصر لغة جديد يشبه الوظيفة يسمى try
ويطلق عليه توقيع
func try(expr) (T1, T2, ... Tn)
حيث تعني expr
تعبيرًا عن معلمة إدخال (عادةً ما تكون استدعاء دالة) تقوم بإرجاع قيم n + 1 لأنواع T1, T2, ... Tn
error
للقيمة الأخيرة. إذا كانت قيمة expr
هي قيمة فردية (n = 0) ، فيجب أن تكون هذه القيمة من النوع type وأن لا تُرجع try
نتيجة. استدعاء try
باستخدام تعبير لا يُرجع القيمة الأخيرة error
type ينتج عنه error
في الترجمة.
لا يمكن استخدام إنشاء try
إلا في دالة تُرجع قيمة واحدة على الأقل ، وتكون قيمة الإرجاع الأخيرة الخاصة بها من النوع type error
. استدعاء try
في سياقات أخرى يؤدي إلى خطأ ترجمة.
استدعاء دالة مع الدالة f()
كما في المثال
x1, x2, … xn = try(f())
يؤدي إلى الكود التالي:
t1, … tn, te := f()
بمعنى آخر ، إذا كان نوع error
الأخير الذي تم إرجاعه بواسطة expr
هو nil
، try
ببساطة إرجاع القيم n الأولى ، وإزالة nil
النهائي.
إذا كانت القيمة الأخيرة التي تم إرجاعها بواسطة expr
غير nil
، فعندئذٍ:
- قيمة إرجاع
error
للدالة المضمّنة (في الرمز الكاذب أعلاه المسماة err
، على الرغم من أن هذا يمكن أن يكون أي معرف أو قيمة إرجاع غير مسماة) تتلقى قيمة الخطأ التي تم إرجاعها من expr
- هناك خروج من وظيفة المغلف
- إذا كانت الدالة المغلقة تحتوي على معلمات إرجاع إضافية ، تحتفظ هذه المعلمات بالقيم التي تم تضمينها فيها قبل استدعاء
try
. - إذا كانت الدالة المرفقة تحتوي على معلمات إرجاع غير محددة إضافية ، فسيتم إرجاع قيم الصفر المقابلة لها (وهو مطابق لحفظ قيم الصفر الأصلية التي تمت تهيئتها بها).
إذا try
استخدام try
في تعيينات متعددة ، كما في المثال أعلاه ، وخطأ غير صفري (يشار إليه فيما يلي بعدم الصفر - تقريبًا لكل.) تم الكشف ، لا يتم تنفيذ الواجب (حسب متغيرات المستخدم) ولا يتغير أي من المتغيرات على الجانب الأيسر من المهمة. بمعنى ، try
تتصرف مثل استدعاء دالة: نتائجها متوفرة فقط إذا try
إرجاع التحكم إلى المتصل (على عكس الحالة مع إرجاع من الدالة المرفقة). نتيجة لذلك ، إذا كانت المتغيرات على الجانب الأيسر من الواجب هي معلمات إرجاع ، فإن استخدام try
سيؤدي إلى سلوك مختلف عن الكود النموذجي الذي يتم مواجهته الآن. على سبيل المثال ، إذا تم تسمية a,b, err
بمعلمات الإرجاع الخاصة بالدالة المرفقة ، فإليك هذا الرمز:
a, b, err = f() if err != nil { return }
سيقوم دائمًا بتعيين قيم للمتغيرات a, b
و err
، بغض النظر عما إذا كانت المكالمة إلى f()
أرجعت خطأ أم لا. التحدي المعاكس
a, b = try(f())
في حالة وجود خطأ ، اترك a
و b
تغيير. على الرغم من حقيقة أن هذا هو فارق بسيط ، فإننا نعتقد أن مثل هذه الحالات نادرة للغاية. إذا كان سلوك الواجب غير المشروط مطلوبًا ، فيجب عليك الاستمرار في استخدام تعبيرات if
.
استخدام
يخبرك تعريف try
بشكل صريح بكيفية استخدامها: يمكن الاستعاضة عن الكثير من التعبيرات التي تحقق من وجود أخطاء. على سبيل المثال:
f, err := os.Open(filename) if err != nil { return …, err
يمكن تبسيطها ل
f := try(os.Open(filename))
إذا لم تُرجع وظيفة الاتصال خطأً ، فلا يمكن استخدام try
(انظر قسم المناقشة). في هذه الحالة ، يجب معالجة الخطأ على أي حال محليًا (نظرًا لعدم وجود إرجاع خطأ) ، وفي هذه الحالة ، if
ظلت الآلية المناسبة للتحقق من الأخطاء.
بشكل عام ، هدفنا هو عدم استبدال كل عمليات التحقق من الأخطاء المحتملة try
. التعليمات البرمجية التي تتطلب دلالات مختلفة يمكن ويجب أن تستمر في استخدام if
التعبيرات والمتغيرات الصريحة مع قيم الخطأ.
اختبار ومحاولة
في إحدى محاولاتنا السابقة لكتابة مواصفات (انظر قسم التكرار التصميمي أدناه) ، تم تصميم try
للذعر عند حدوث خطأ عند استخدامها داخل إحدى الوظائف دون حدوث خطأ في الإرجاع. سمح باستخدام try
في اختبارات الوحدة بناءً على حزمة testing
الخاصة بالمكتبة القياسية.
كأحد الخيارات ، من الممكن استخدام وظائف الاختبار مع التواقيع في حزمة testing
func TestXxx(*testing.T) error func BenchmarkXxx(*testing.B) error
من أجل السماح باستخدام try
في الاختبارات. دالة اختبار تقوم بإرجاع خطأ غير صفري سوف تستدعي ضمنيًا t.Fatal(err)
أو b.Fatal(err)
. هذا تغيير مكتبة صغير يتجنب الحاجة إلى سلوكيات مختلفة (رجوع أو ذعر) try
، اعتمادًا على السياق.
أحد عيوب هذا النهج هو أن t.Fatal
و b.Fatal
لن يكونا قادرين على إرجاع رقم السطر الذي سقط عليه الاختبار. عيب آخر هو أنه يجب علينا تغيير الاختبارات بطريقة أو بأخرى. الحل لهذه المشكلة هو سؤال مفتوح. لا نقترح تغييرات محددة على حزمة testing
في هذا المستند.
انظر أيضًا رقم 21111 ، مما يشير إلى السماح لوظائف المثال بإرجاع خطأ.
خطأ في التعامل
كان تصميم المسودة الأصلي مهتمًا إلى حد كبير بدعم اللغة للالتفاف أو زيادة الأخطاء. اقترحت المسودة مقبضًا جديدًا للكلمات الرئيسية وطريقة جديدة للإعلان عن معالجات الأخطاء . جذبت هذه اللغة الجديدة مشاكل مثل الذباب بسبب دلالات غير تافهة ، وخاصة عند النظر في تأثيرها على تدفق التنفيذ. على وجه الخصوص ، عبرت وظيفة handle
فشلاً ذريعًا مع وظيفة defer
، مما جعل ميزة اللغة الجديدة غير متعامدة مع أي شيء آخر.
هذا الاقتراح يقلل من مشروع التصميم الأصلي إلى جوهره. إذا كان إثراء الخطأ أو الالتفاف مطلوبًا ، فهناك طريقتان: إرفاق بـ if err != nil { return err}
، أو "إعلان" معالج خطأ داخل تعبير defer
:
defer func() { if err != nil {
في هذا المثال ، err
اسم معلمة الإرجاع error
النوع للدالة المرفقة.
في الممارسة العملية ، نتخيل وظائف المساعد مثل
func HandleErrorf(err *error, format string, args ...interface{}) { if *err != nil { *err = fmt.Errorf(format + ": %v", append(args, *err)...) } }
أو شيء مشابه. يمكن أن تصبح حزمة fmt
مكانًا طبيعيًا لهؤلاء المساعدين (توفر بالفعل fmt.Errorf
). باستخدام المساعدين ، سيتم تخفيض تعريف معالج الأخطاء في كثير من الحالات إلى سطر واحد. على سبيل المثال ، لإثراء الخطأ من وظيفة "نسخة" ، يمكنك الكتابة
defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
إذا كان fmt.HandleErrorf
يضيف معلومات الخطأ ضمنيًا ، فمن السهل جدًا قراءة هذا البناء وله ميزة أنه يمكن تنفيذه دون إضافة عناصر جديدة في بناء جملة اللغة.
العيب الرئيسي لهذا النهج هو أنه يجب تسمية معلمة الخطأ المرتجع ، مما قد يؤدي إلى واجهة برمجة تطبيقات أقل دقة (راجع الأسئلة الشائعة حول هذا الموضوع). نحن نعتقد أننا سوف تعتاد على ذلك عندما يتم تأسيس النمط المناسب من رمز الكتابة.
تأجيل الكفاءة
أحد الاعتبارات المهمة عند استخدام defer
للخطأ هو الكفاءة. يعتبر التعبير defer
بطيئًا . لا نريد الاختيار بين الشفرة الفعالة ومعالجة الأخطاء الجيدة. بصرف النظر عن هذا الاقتراح ، ناقش فريقا وقت التشغيل "Go" وفريق التحويل البرمجي طرق التنفيذ البديلة ، ونعتقد أنه يمكننا أن نجعل طرقًا نموذجية لاستخدام الإرجاء لمعالجة الأخطاء المشابهة في الكفاءة للرمز "اليدوي" الحالي. نأمل أن نضيف تطبيقًا أسرع للتأجيل في Go 1.14 (انظر أيضًا تذكرة CL 171158 ، وهي الخطوة الأولى في هذا الاتجاه).
حالات خاصة go try(f), defer try(f)
تبدو بنية try
كدالة ، ولهذا السبب ، من المتوقع استخدامه في أي مكان تكون فيه استدعاء دالة مقبولة. ومع ذلك ، إذا تم استخدام call try
في عبارة go
، فستكون الأمور معقدة:
go try(f())
هنا يتم تنفيذ f()
عند تنفيذ تعبير go في goroutine الحالي ، يتم تمرير نتائج استدعاء f
كوسيطات try
، والتي تبدأ في goroutine الجديد. إذا f
خطأ غير صفري ، فمن المتوقع أن try
العودة من الدالة المغلقة ؛ ومع ذلك ، لا توجد وظيفة (وليس هناك أي معلمة إرجاع من error
type) ، لأن يتم تنفيذ الرمز في goroutine منفصلة. لهذا السبب ، نقترح تعطيل try
في تعبير go
.
الوضع مع
defer try(f())
يبدو مشابهاً ، ولكن دلالات التأجيل تعني هنا أن تنفيذ try
سوف يتأخر حتى يعود من الوظيفة المغلقة. كما كان من قبل ، f()
تقييم f()
عند defer
التأجيل ، ويتم تمرير نتائجها إلى try
المؤجلة.
try
التحقق من الخطأ f()
إرجاعه فقط في اللحظة الأخيرة قبل العودة من الدالة المغلقة. بدون تغيير سلوك try
، يمكن لهذا الخطأ الكتابة فوق قيمة خطأ أخرى تحاول الدالة المغلقة إرجاعها. هذا في أحسن الأحوال يخلط ، في أسوأ الأحوال أنه يثير أخطاء. لهذا السبب ، نقترح عليك حظر الاتصال في defer
أيضًا. يمكننا دائمًا إعادة النظر في هذا القرار إذا كان هناك تطبيق معقول لهذه الدلالات.
أخيرًا ، مثل بقية الإنشاءات المضمّنة ، يمكن استخدام try
فقط كمكالمة. لا يمكن استخدامه كدالة قيمة أو في تعبير تعيين متغير كما في f := try
(تمامًا مثل f := print
و f := new
ممنوع).
المناقشة
تكرارات التصميم
فيما يلي مناقشة مختصرة للتصاميم السابقة التي أدت إلى الحد الأدنى الحالي من الاقتراح. نأمل أن يلقي هذا الضوء على قرارات التصميم المحددة.
استلهم تكرارنا الأول من هذه الجملة فكرتين من مقالة "الأجزاء الأساسية لمعالجة الأخطاء" ، وهي استخدام الوظيفة المدمجة بدلاً من المشغل ووظيفة Go المعتادة للتعامل مع الأخطاء بدلاً من إنشاء لغة جديدة. على عكس هذا المنشور ، كان لدى معالج الأخطاء لدينا خطأ في توقيع ثابت func(error) error
لتبسيط الأمور. سوف يتم استدعاء معالج الأخطاء بواسطة الدالة try
إذا كان هناك خطأ قبل أن try
الخروج من الدالة المغلقة. هنا مثال:
handler := func(err error) error { return fmt.Errorf("foo failed: %v", err)
على الرغم من أن هذا النهج سمح بتعريف معالجات الأخطاء الفعالة المعرفة من قبل المستخدم ، فقد أثار أيضًا العديد من الأسئلة التي من الواضح أنها لم تكن تحتوي على الإجابات الصحيحة: ما الذي يجب أن يحدث إذا تم نقل الصفر إلى المعالج؟ يجب أن try
الذعر أو النظر إلى هذا على أنه عدم وجود معالج؟ ماذا لو تم استدعاء المعالج مع خطأ غير صفري ثم إرجاع نتيجة فارغة؟ هل هذا يعني أن الخطأ "ملغى"؟ أو هل يجب أن تقوم دالة الإحاطة بإرجاع خطأ فارغ؟ كانت هناك شكوك أيضًا في أن النقل الاختياري لمعالج الأخطاء سيشجع المطورين على تجاهل الأخطاء بدلاً من تصحيحها. سيكون من السهل أيضًا تنفيذ الخطأ الصحيح في كل مكان ، لكن تخطي استخدامًا واحدًا try
. وما شابه ذلك.
في التكرار التالي ، تمت إزالة القدرة على اجتياز معالج أخطاء مخصص لصالح استخدام defer
التفاف الأخطاء. هذا يبدو وكأنه نهج أفضل لأنه جعل معالجات الأخطاء أكثر وضوحا في التعليمات البرمجية المصدر. ألغت هذه الخطوة أيضًا جميع المشكلات المتعلقة بالنقل الاختياري لوظائف المعالج ، ولكنها طلبت تسمية المعلمات التي تم إرجاعها بنوع error
إذا كان الوصول مطلوبًا (قررنا أن هذا أمر طبيعي). علاوة على ذلك ، في محاولة لجعل try
مفيدة ليس فقط داخل الوظائف التي تُرجع الأخطاء ، كان من الضروري جعل سلوك try
حساسًا للسياق: إذا try
استخدام try
على مستوى الحزمة ، أو إذا تم استدعاؤها داخل دالة لا تُرجع خطأً ، try
الذعر تلقائيًا عند اكتشاف خطأ. (وكتأثير جانبي ، وبسبب هذه الخاصية ، تم استدعاء بناء اللغة must
بدلاً من try
في هذه الجملة.) بدا السلوك الحساس للسياق try
(أو must
) طبيعيًا ومفيدًا أيضًا: فهو سيؤدي إلى القضاء على العديد من الوظائف المعرفة من قبل المستخدم والمستخدمة في التعبيرات تهيئة متغيرات الحزمة. كما أنه فتح إمكانية استخدام الاختبارات في وحدة الاختبار مع حزمة testing
.
ومع ذلك ، فإن السلوك الحساس للسياق try
كان محفوفًا بالأخطاء: على سبيل المثال ، قد يتغير سلوك الدالة التي تستخدمها بهدوء (ذعر أو لا) عند إضافة أو إزالة خطأ الإرجاع إلى توقيع الوظيفة. هذا يبدو خطرا جدا الممتلكات. كان الحل الواضح هو تقسيم وظيفة try
إلى وظيفتين منفصلتين must
تجربتهما وتجربتهما (تشبه إلى حد كبير الطريقة التي تم اقتراحها في # 31442 ). ومع ذلك ، قد يتطلب ذلك وظيفتين مضمنتين ، بينما ترتبط try
فقط مباشرة بدعم أفضل لمعالجة الأخطاء.
لذلك ، في التكرار الحالي ، بدلاً من تضمين الوظيفة المضمنة الثانية ، قررنا إزالة الدلالات المزدوجة try
، وبالتالي السماح باستخدامها فقط في الوظائف التي تُرجع خطأً.
ميزات التصميم المقترح
هذا الاقتراح قصير جدًا وقد يبدو خطوة إلى الوراء مقارنة بمسودة العام الماضي. نعتقد أن الحلول المحددة لها ما يبررها:
أول الأشياء أولاً ، try
تمامًا نفس دلالات بيان check
المقترحة في الأصل بدون handle
. هذا يؤكد دقة المسودة الأصلية في أحد الجوانب المهمة.
إن اختيار وظيفة مدمجة بدلاً من عوامل التشغيل له العديد من المزايا. لا يتطلب كلمة رئيسية جديدة مثل check
، مما يجعل التصميم غير متوافق مع المحللون الحاليين. ليست هناك حاجة أيضًا لتوسيع بناء جملة التعبيرات باستخدام مشغل جديد. تعد إضافة وظيفة مدمجة جديدة أمرًا بسيطًا نسبيًا ومتعامد تمامًا مع ميزات اللغة الأخرى.
يتطلب استخدام دالة مضمّنة بدلاً من عامل تشغيل استخدام الأقواس. يجب أن نكتب try(f())
بدلاً من try f()
. هذا هو السعر (الصغير) الذي يتعين علينا دفعه للتوافق مع الإصدارات السابقة مع المحللون الحاليون. ومع ذلك ، فإن هذا يجعل التصميم متوافقًا أيضًا مع الإصدارات المستقبلية: إذا قررنا أن تمرير وظيفة ما بطريقة خطأ أو إضافة معلمة إضافية try
لهذا الغرض فكرة جيدة ، فإن إضافة وسيطة إضافية إلى استدعاء المكالمة ستكون تافهة.
كما اتضح فيما بعد ، فإن الحاجة إلى كتابة الأقواس لها مزاياها. try
, , :
info := try(try(os.Open(file)).Stat())
try
, : try
, .. try
(receiver) .Stat
( os.Open
).
try
, : os.Open(file)
.. try
( , try
os
, , try
try
).
, .. .
. , . defer
, .
Go - , . , Go append
. append
, . , . , try
.
, , Go : panic
recover
. error
try
.
, try
, , — — , . Go:
, , . if
-.
تطبيق
:
- Go.
try
. , . .go/types
try
. .gccgo
. ( , ).- .
- , . , . .
Robert Griesemer go/types
, () cmd/compile
. , Go 1.14, 1 2019.
, Ian Lance Taylor gccgo
, .
"Go 2, !" , .
1 , , , Go 1.14 .
أمثلة
CopyFile
:
func CopyFile(src, dst string) (err error) { defer func() { if err != nil { err = fmt.Errorf("copy %s %s: %v", src, dst, err) } }() r := try(os.Open(src)) defer r.Close() w := try(os.Create(dst)) defer func() { w.Close() if err != nil { os.Remove(dst)
, " ", defer
:
defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)
( defer
-), defer
, .
printSum
func printSum(a, b string) error { x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil }
:
func printSum(a, b string) error { fmt.Println( "result:", try(strconv.Atoi(a)) + try(strconv.Atoi(b)), ) return nil }
main
:
func localMain() error { hex := try(ioutil.ReadAll(os.Stdin)) data := try(parseHexdump(string(hex))) try(os.Stdout.Write(data)) return nil } func main() { if err := localMain(); err != nil { log.Fatal(err) } }
- try
, :
n, err := src.Read(buf) if err == io.EOF { break } try(err)
, .
: ?
: check
handle
, . , handle
defer
, handle
.
: try ?
: try
Go . - , . , . , " ". try
, .. .
: try
try?
: , check
, must
do
. try
, . try
check
(, ), - . . must
; try
— . , Rust Swift try
( ). .
: ?
Rust?
: Go ; , Go ( ; - ). , ?
, . , , , (package, interface, if, append, recover, ...), , (struct, var, func, int, len, image, ..). Rust ?
try
— Go, , ( ) . , ?
. , , (, ..) . . , .
: ( error) , defer , go doc. ?
: go doc
, - ( _
) , . , func f() (_ A, _ B, err error)
go doc
func f() (A, B, error)
. , , , . , , . , , , -, (deferred) . Jonathan Geddes try()
.
: defer ?
: defer
. , , defer "" . . CL 171758 , defer 30%.
: ?
: , . , ( , ), . defer
, . defer
- https://golang.org/issue/29934 ( Go 2), .
: , try, error. , ?
: error
( ) , , nil
. try
. ( , . - ).
: Go , try ?
: try
, try
. super return
-, try
Go
. try
. .
: try , . ?
: try
; , . try
( ), . , if
.
: , . try, defer . ?
: , . .
: try
( catch
)?
: try
— ("") , , ( ) . try
; . . "" . , . , try
— . , , throw
try-catch
Go. , (, ), ( ) , . "" try-catch
, . , , . Go . panic
, .