Go: الاقتراح: Go 2: تبسيط معالجة الأخطاء باستخدام || يخطئ لاحقة

تم إنشاؤها على ٢٥ يوليو ٢٠١٧  ·  519تعليقات  ·  مصدر: golang/go

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

if err != nil {
    return err
}

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

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

  1. تجاهل الخطأ
  2. إرجاع الخطأ غير معدل
  3. إرجاع الخطأ بمعلومات سياقية إضافية

من السهل بالفعل (ربما من السهل جدًا) تجاهل الخطأ (انظر # 20803). تعمل العديد من المقترحات الحالية الخاصة بمعالجة الأخطاء على تسهيل إعادة الخطأ بدون تعديل (على سبيل المثال ، # 16225 ، # 18721 ، # 21146 ، # 21155). القليل يسهل إرجاع الخطأ بمعلومات إضافية.

يعتمد هذا الاقتراح بشكل فضفاض على لغات Perl و Bourne shell ، وهما مصادر خصبة للأفكار اللغوية. نقدم نوعًا جديدًا من العبارات ، مشابهًا لتعبير التعبير: تعبير استدعاء متبوعًا بعلامة || . القواعد هي:

PrimaryExpr Arguments "||" Expression

وبالمثل نقدم نوعًا جديدًا من بيان التخصيص:

ExpressionList assign_op PrimaryExpr Arguments "||" Expression

بالرغم من أن القواعد تقبل أي نوع بعد || في حالة عدم التخصيص ، فإن النوع الوحيد المسموح به هو النوع المحدد مسبقًا error . يجب أن يكون التعبير الذي يلي || نوعًا يمكن تخصيصه لـ error . قد لا يكون نوعًا منطقيًا ، ولا حتى نوعًا منطقيًا مسمى يمكن تخصيصه لـ error . (هذا التقييد الأخير مطلوب لجعل هذا الاقتراح متوافقًا مع اللغة الحالية.)

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

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

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

هذا هو الاقتراح الكامل.

على سبيل المثال ، الوظيفة os.Chdir حاليًا

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

بموجب هذا الاقتراح ، يمكن كتابته كـ

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir, err}
    return nil
}

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

FrozenDueToAge Go2 LanguageChange NeedsInvestigation Proposal error-handling

التعليق الأكثر فائدة

الفكرة البسيطة ، مع دعم زخرفة الأخطاء ، ولكن تتطلب تغييرًا أكثر جذرية للغة (من الواضح أنه ليس من أجل go1.10) هي تقديم كلمة رئيسية جديدة check .

سيكون له شكلين: check A و check A, B .

يجب أن يكون كلا من A و B error . لن يتم استخدام النموذج الثاني إلا عند التزيين بالخطأ ؛ سيستخدم الأشخاص الذين لا يحتاجون أو يرغبون في تزيين أخطائهم الشكل الأبسط.

النموذج الأول (تحقق أ)

check A بتقييم A . إذا كان nil ، فلن يفعل شيئًا. إذا لم يكن nil ، فإن check يتصرف مثل return {<zero>}*, A .

أمثلة

  • إذا قامت الدالة بإرجاع خطأ فقط ، فيمكن استخدامها مع check ، لذا
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

يصبح

check UpdateDB()
  • بالنسبة لدالة ذات قيم إرجاع متعددة ، ستحتاج إلى تعيينها ، كما نفعل الآن.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

يصبح

a, b, err := Foo()
check err

// use a and b

النموذج الثاني (الاختيار أ ، ب)

check A, B يقيّم A . إذا كان nil ، فلن يفعل شيئًا. إذا لم يكن nil ، فإن check يتصرف مثل return {<zero>}*, B .

هذا لاحتياجات تزيين الخطأ. ما زلنا نتحقق من A ، ولكنه B المستخدم في return الضمني.

مثال

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

يصبح

a, err := Foo()
check err, &BarError{"Bar", err}

ملحوظات

إنه خطأ تجميع لـ

  • استخدم العبارة check للأشياء التي لا error بـ
  • استخدم check في دالة ذات قيم إرجاع ليست بالصيغة { type }*, error

نموذج الاختصاصين check A, B قصير الدائرة. لا يتم تقييم B إذا كان A هو nil .

ملاحظات على التطبيق العملي

هناك دعم لأخطاء التزيين ، لكنك تدفع مقابل بناء الجملة clunkier check A, B فقط عندما تحتاج بالفعل إلى تزيين الأخطاء.

بالنسبة إلى النموذج المعياري if err != nil { return nil, nil, err } (وهو أمر شائع جدًا ) ، فإن check err موجز بقدر ما يمكن أن يكون دون التضحية بالوضوح (انظر الملاحظة حول بناء الجملة أدناه).

ملاحظات على النحو

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

الجانب السلبي لأفكار مثل <do-stuff> || <handle-err> و <do-stuff> catch <handle-err> أعلاه ، أو a, b = foo()? المقترح في سلسلة رسائل أخرى ، هو أنها تخفي تعديل تدفق التحكم بطريقة تجعل التدفق أكثر صعوبة للمتابعة؛ الأول مع آلية || <handle-err> ملحقة في نهاية سطر عادي المظهر ، والأخير برمز صغير يمكن أن يظهر في كل مكان ، بما في ذلك ونهاية سطر عادي المظهر ، ربما عدة مرات.

ستظل عبارة check دائمًا في المستوى الأعلى في الكتلة الحالية ، ولها نفس أهمية العبارات الأخرى التي تعدل تدفق التحكم (على سبيل المثال ، return مبكرًا).

ال 519 كومينتر

    syscall.Chdir(dir) || &PathError{"chdir", dir, e}

من أين يأتي e من هناك؟ خطأ مطبعي؟

أو هل تقصد:

func Chdir(dir string) (e error) {
    syscall.Chdir(dir) || &PathError{"chdir", dir, e}
    return nil
}

(أي ، هل الخطأ الضمني! = لا يتحقق أولاً من تعيين خطأ لمعامل النتيجة ، والذي يمكن تسميته لتعديله مرة أخرى قبل الإرجاع الضمني؟)

تنهد ، أفسدت المثال الخاص بي. تم إصلاحه الآن: e يجب أن يكون err . يضع الاقتراح err في النطاق للاحتفاظ بقيمة الخطأ لاستدعاء الوظيفة عندما لا تكون في عبارة التخصيص.

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

قد يكون هذا موضع اهتمام davecheney ، الذي كتب https://github.com/pkg/errors.

ماذا يحدث في هذا الكود:

if foo, err := thing.Nope() || &PathError{"chdir", dir, err}; err == nil || ignoreError {
}

(أعتذر إذا لم يكن هذا ممكنًا حتى بدون الجزء || &PathError{"chdir", dir, e} ؛ أحاول التعبير عن أن هذا يبدو وكأنه تجاوز محير للسلوك الحالي ، والعوائد الضمنية ... متستر؟)

@ object88 سأكون على ما يرام إذا لم أسمح بهذه الحالة الجديدة في SimpleStmt كما هو مستخدم في كشوف الحسابات if و for و switch . من المحتمل أن يكون هذا هو الأفضل على الرغم من أنه سيعقد القواعد قليلاً.

ولكن إذا لم نفعل ذلك ، فما يحدث هو أنه إذا thing.Nope() خطأ غير صفري ، فإن وظيفة الاستدعاء تعود بـ &PathError{"chdir", dir, err} (حيث err هو متغير تم تعيينه بواسطة الاستدعاء إلى thing.Nope() ). إذا أرجع thing.Nope() خطأ nil ، فإننا نعلم بالتأكيد أن err == nil صحيح في حالة if ، وبالتالي فإن نص إذا تم تنفيذ البيان. لا يتم قراءة المتغير ignoreError أبدًا. لا يوجد غموض أو تجاوز للسلوك الحالي هنا ؛ يتم قبول معالجة || المقدمة هنا فقط عندما لا يكون التعبير بعد || قيمة منطقية ، مما يعني أنه لن يتم تجميعها حاليًا.

أنا أوافق على أن العوائد الضمنية متستر.

نعم ، مثالي فقير جدًا. لكن عدم السماح بالعملية داخل if أو for أو switch سيحل الكثير من الالتباس المحتمل.

نظرًا لأن الشريط المطلوب النظر فيه بشكل عام أمر يصعب القيام به في اللغة كما هو ، فقد قررت أن أرى مدى صعوبة ترميز هذا المتغير في اللغة. ليس أصعب بكثير من الآخرين: https://play.golang.org/p/9B3Sr7kj39

أنا لا أحب حقًا كل هذه المقترحات لجعل نوعًا واحدًا من القيمة وموضعًا واحدًا في حجج الإرجاع خاصين. هذا في بعض النواحي أسوأ في الواقع لأنه يجعل أيضًا err اسمًا خاصًا في هذا السياق المحدد.

على الرغم من أنني أوافق بالتأكيد على أن الناس (بمن فيهم أنا!) يجب أن يكونوا أكثر ضجرًا من إرجاع الأخطاء دون سياق إضافي.

عندما تكون هناك قيم إرجاع أخرى ، مثل

if err != nil {
  return 0, nil, "", Struct{}, wrap(err)
}

من المؤكد أنها يمكن أن تكون متعبة للقراءة. لقد أحببت إلى حد ما اقتراح nigeltao مقابل return ..., err في https://github.com/golang/go/issues/19642#issuecomment -288559297

إذا فهمت بشكل صحيح ، لبناء شجرة بناء الجملة ، سيحتاج المحلل اللغوي إلى معرفة أنواع المتغيرات للتمييز بين

boolean := BoolFunc() || BoolExpr

و

err := FuncReturningError() || Expr

لا تبدو جيدة.

الاقل هو الاكثر...

عندما تحتوي قائمة ExpressionList المرتجعة على عنصرين خام أكثر ، كيف تعمل؟

راجع للشغل ، أريد الذعر إذا بدلاً من ذلك.

err := doSomeThing()
panicIf(err)

err = doAnotherThing()
panicIf(err)

ianlancetaylor في err لا يزال غير مصرح به صراحة ، وتم سحبه باعتباره "سحريًا" (لغة محددة مسبقًا) ، أليس كذلك؟

أم سيكون شيء من هذا القبيل

func Chdir(dir string) error {
    return (err := syscall.Chdir(dir)) || &PathError{"chdir", dir, err}
}

؟

من ناحية أخرى (نظرًا لأنه تم وضع علامة "تغيير اللغة" عليه بالفعل ...)
قدم عامل تشغيل جديد (!! أو ؟؟) يقوم باختصار الخطأ! = لا شيء (أو أي شيء لاغى؟)

func DirCh(dir string) (string, error) {
    return dir, (err := syscall.Chdir(dir)) !! &PathError{"chdir", dir, err}
}

آسف إذا كان هذا بعيد جدا :)

أوافق على أن معالجة الأخطاء في Go يمكن أن تكون متكررة. لا أمانع التكرار ، لكن الكثير منها يؤثر على قابلية القراءة. هناك سبب يجعل "التعقيد السيكلومي" (سواء كنت تؤمن به أم لا) يستخدم تدفقات التحكم كمقياس للتعقيد. تضيف عبارة "if" ضوضاء إضافية.

ومع ذلك ، فإن الصيغة المقترحة "||" ليس من السهل جدًا قراءته ، خاصة وأن الرمز يُعرف عمومًا باسم عامل التشغيل OR. بالإضافة إلى ذلك ، كيف تتعامل مع الدوال التي تُرجع قيمًا وأخطاء متعددة؟

أنا فقط أرمي بعض الأفكار هنا. ماذا عن استخدام الخطأ كمخرجات بدلاً من استخدام الخطأ كمدخل؟ مثال: https://play.golang.org/p/rtfoCIMGAb

شكرا لجميع التعليقات.

opennota نقطة جيدة. لا يزال من الممكن أن تعمل ولكني أوافق على أن الجانب محرج.

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

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

tandr نعم ، يتم تعريف err بطريقة سحرية ، وهو أمر فظيع جدًا. الاحتمال الآخر هو السماح لتعبير الخطأ باستخدام error للإشارة إلى الخطأ ، وهو أمر مروع بطريقة مختلفة.

tandr يمكننا استخدام عامل تشغيل مختلف ولكني لا أرى أي ميزة كبيرة. لا يبدو أنه يجعل النتيجة أكثر قابلية للقراءة.

henryas أعتقد أن الاقتراح يشرح كيفية

henryas شكرا على المثال. الشيء الذي لا يعجبني في هذا النوع من النهج هو أنه يجعل الخطأ يعالج الجانب الأبرز من الكود. أريد أن يكون معالجة الخطأ موجودًا ومرئيًا ولكني لا أريد أن يكون أول شيء على الخط. هذا صحيح اليوم ، مع المصطلح if err != nil والمسافة البادئة لرمز معالجة الخطأ ، ويجب أن يظل صحيحًا إذا تمت إضافة أي ميزات جديدة لمعالجة الأخطاء.

شكرا لك مرة أخرى.

ianlancetaylor لا أعرف ما إذا كنت قد سحبت رابط الملعب الخاص بي ولكن كان هناك "ذعر" مما يتيح لك إضافة سياق إضافي.

سأعيد إنتاج نسخة مبسطة إلى حد ما هنا:

func panicIf(err error, transforms ...func(error) error) {
  if err == nil {
    return
  }
  for _, transform := range transforms {
    err = transform(err)
  }
  panic(err)
}

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

func DirCh(dir string) (string, error) {
    dir := syscall.Chdir(dir)        =: err; if err != nil { return "", err }
}

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

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

أنا حقا أحب الصيغة المقترحة من قبل billyh هنا

func Chdir(dir string) error {
    e := syscall.Chdir(dir) catch: &PathError{"chdir", dir, e}
    return nil
}

أو مثال أكثر تعقيدًا باستخدام https://github.com/pkg/errors

func parse(input io.Reader) (*point, error) {
    var p point

    err := read(&p.Longitude) catch: nil, errors.Wrap(err, "Failed to read longitude")
    err = read(&p.Latitude) catch: nil, errors.Wrap(err, "Failed to read Latitude")
    err = read(&p.Distance) catch: nil, errors.Wrap(err, "Failed to read Distance")
    err = read(&p.ElevationGain) catch: nil, errors.Wrap(err, "Failed to read ElevationGain")
    err = read(&p.ElevationLoss) catch: nil, errors.Wrap(err, "Failed to read ElevationLoss")

    return &p, nil
}

الفكرة البسيطة ، مع دعم زخرفة الأخطاء ، ولكن تتطلب تغييرًا أكثر جذرية للغة (من الواضح أنه ليس من أجل go1.10) هي تقديم كلمة رئيسية جديدة check .

سيكون له شكلين: check A و check A, B .

يجب أن يكون كلا من A و B error . لن يتم استخدام النموذج الثاني إلا عند التزيين بالخطأ ؛ سيستخدم الأشخاص الذين لا يحتاجون أو يرغبون في تزيين أخطائهم الشكل الأبسط.

النموذج الأول (تحقق أ)

check A بتقييم A . إذا كان nil ، فلن يفعل شيئًا. إذا لم يكن nil ، فإن check يتصرف مثل return {<zero>}*, A .

أمثلة

  • إذا قامت الدالة بإرجاع خطأ فقط ، فيمكن استخدامها مع check ، لذا
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

يصبح

check UpdateDB()
  • بالنسبة لدالة ذات قيم إرجاع متعددة ، ستحتاج إلى تعيينها ، كما نفعل الآن.
a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

يصبح

a, b, err := Foo()
check err

// use a and b

النموذج الثاني (الاختيار أ ، ب)

check A, B يقيّم A . إذا كان nil ، فلن يفعل شيئًا. إذا لم يكن nil ، فإن check يتصرف مثل return {<zero>}*, B .

هذا لاحتياجات تزيين الخطأ. ما زلنا نتحقق من A ، ولكنه B المستخدم في return الضمني.

مثال

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

يصبح

a, err := Foo()
check err, &BarError{"Bar", err}

ملحوظات

إنه خطأ تجميع لـ

  • استخدم العبارة check للأشياء التي لا error بـ
  • استخدم check في دالة ذات قيم إرجاع ليست بالصيغة { type }*, error

نموذج الاختصاصين check A, B قصير الدائرة. لا يتم تقييم B إذا كان A هو nil .

ملاحظات على التطبيق العملي

هناك دعم لأخطاء التزيين ، لكنك تدفع مقابل بناء الجملة clunkier check A, B فقط عندما تحتاج بالفعل إلى تزيين الأخطاء.

بالنسبة إلى النموذج المعياري if err != nil { return nil, nil, err } (وهو أمر شائع جدًا ) ، فإن check err موجز بقدر ما يمكن أن يكون دون التضحية بالوضوح (انظر الملاحظة حول بناء الجملة أدناه).

ملاحظات على النحو

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

الجانب السلبي لأفكار مثل <do-stuff> || <handle-err> و <do-stuff> catch <handle-err> أعلاه ، أو a, b = foo()? المقترح في سلسلة رسائل أخرى ، هو أنها تخفي تعديل تدفق التحكم بطريقة تجعل التدفق أكثر صعوبة للمتابعة؛ الأول مع آلية || <handle-err> ملحقة في نهاية سطر عادي المظهر ، والأخير برمز صغير يمكن أن يظهر في كل مكان ، بما في ذلك ونهاية سطر عادي المظهر ، ربما عدة مرات.

ستظل عبارة check دائمًا في المستوى الأعلى في الكتلة الحالية ، ولها نفس أهمية العبارات الأخرى التي تعدل تدفق التحكم (على سبيل المثال ، return مبكرًا).

ALTree ، لم أفهم كيف

a, b, err := Foo()
check err

يحقق العائد الثلاثي القيم من الأصل:

return "", "", err

هل يتم إرجاع قيم صفرية فقط لجميع المرتجعات المعلنة باستثناء الخطأ النهائي؟ ماذا عن الحالات التي تريد فيها إرجاع قيمة صالحة مع خطأ ، على سبيل المثال عدد البايتات المكتوبة عند فشل الكتابة ()؟

أيًا كان الحل الذي نتبعه ، يجب أن يقيد عمومية معالجة الأخطاء إلى الحد الأدنى.

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

billyh هذا موضح أعلاه ، على السطر الذي يقول:

إذا لم يكن لا شيء ، تحقق من الأفعال مثل return {<zero>}*, A

سيعيد check القيمة الصفرية لأي قيمة إرجاع ، باستثناء الخطأ (في الموضع الأخير).

ماذا عن الحالات التي تريد فيها إرجاع قيمة صالحة مع خطأ

ثم ستستخدم المصطلح if err != nil { .

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

أيًا كان الحل الذي نتبعه ، يجب أن يقيد عمومية معالجة الأخطاء إلى الحد الأدنى.

انظر جوابي أعلاه. لا يزال لديك if وأي شيء آخر توفره لك اللغة الآن.

سيقوم أي محرر حديث إلى حد كبير بتسليط الضوء على الكلمة المحجوزة

يمكن. لكن تقديم بناء جملة معتم ، يتطلب إبراز بناء الجملة ليكون قابلاً للقراءة ، ليس مثاليًا.

يمكن إصلاح هذا الخطأ المحدد عن طريق إدخال وسيلة إرجاع مزدوجة للغة.
في هذه الحالة ، ترجع الدالة a () 123:

func a () int {
ب()
إرجاع 456
}
func ب () {
عودة العودة int (123)
}

يمكن استخدام هذه الميزة لتبسيط معالجة الأخطاء على النحو التالي:

مقبض func (var * foo ، خطأ err) (var * foo ، خطأ err) {
إذا أخطأت! = لا شيء {
عودة العودة لا شيء ، يخطئ
}
عودة فار ، لا شيء
}

func client_code () (* client_object ، خطأ) {
var obj، err = handle (something_that_can_fail ())
// يتم الوصول إلى هذا فقط إذا لم يفشل شيء ما
// وإلا فإن وظيفة client_code ستنشر الخطأ في المكدس
تأكيد (يخطئ == لا شيء)
}

يسمح هذا للأشخاص بكتابة وظائف معالج الأخطاء التي يمكنها نشر الأخطاء في المكدس
يمكن أن تكون وظائف معالجة الأخطاء هذه منفصلة عن الكود الرئيسي

عذرًا إذا كنت قد أخطأت ، لكنني أريد توضيح نقطة ، ستظهر الوظيفة أدناه خطأ أو تحذير vet أم سيتم قبولها؟

func Chdir(dir string) (err error) {
    syscall.Chdir(dir) || err
    return nil
}

rodcorsi بموجب هذا الاقتراح ، سيتم قبول

if err := syscall.Chdir(dir); err != nil {
    return err
}

ماذا عن توسيع استخدام السياق للتعامل مع الخطأ؟ على سبيل المثال ، بالنظر إلى التعريف التالي:
type ErrorContext interface { HasError() bool SetError(msg string) Error() string }
الآن في وظيفة عرضة للخطأ ...
func MyFunction(number int, ctx ErrorContext) int { if ctx.HasError() { return 0 } return number + 1 }
في الوظيفة الوسيطة ...
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) return number }
وفي وظيفة المستوى العلوي
func main() { ctx := context.New() no := MyIntermediateFunction(ctx) if ctx.HasError() { log.Fatalf("Error: %s", ctx.Error()) return } fmt.Printf("%d\n", no) }
هناك العديد من الفوائد باستخدام هذا النهج. أولاً ، لا يشتت انتباه القارئ عن مسار التنفيذ الرئيسي. يوجد حد أدنى من عبارات "if" للإشارة إلى الانحراف عن مسار التنفيذ الرئيسي.

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

ثالثًا ، يتم إرسال الخطأ تلقائيًا إلى الطرف المعني ، والذي يكون في هذه الحالة مالك السياق. إذا كان هناك خطأ إضافي في المعالجة ، فسيتم عرضه بوضوح. على سبيل المثال ، دعنا نجري بعض التغييرات على الوظيفة الوسيطة لالتفاف أي خطأ موجود:
func MyIntermediateFunction(ctx ErrorContext) int { if ctx.HasError() { return 0 } number := 0 number = MyFunction(number, ctx) number = MyFunction(number, ctx) number = MyFunction(number, ctx) if ctx.HasError() { ctx.SetError(fmt.Sprintf("wrap msg: %s", ctx.Error()) return } number *= 20 number = MyFunction(number, ctx) return number }
في الأساس ، ما عليك سوى كتابة رمز معالجة الأخطاء حسب الحاجة. لا تحتاج إلى إنشاء فقاعات لهم يدويًا.

أخيرًا ، بصفتك كاتب الوظيفة لديك رأي حول ما إذا كان يجب معالجة الخطأ. باستخدام نهج Go الحالي ، من السهل القيام بذلك ...
""
// بالنظر إلى التعريف التالي
func MyFunction (رقم int) خطأ

// ثم افعل هذا
MyFunction (8) // دون التحقق من الخطأ
With the ErrorContext, you as the function owner can make the error checking optional with this:
func MyFunction (ctx ErrorContext) {
إذا ctx! = nil && ctx.HasError () {
إرجاع
}
// ...
}
Or make it compulsory with this:
func MyFunction (ctx ErrorContext) {
إذا كان ctx.HasError () {// سيصاب بالذعر إذا كانت قيمة ctx لا شيء
إرجاع
}
// ...
}
If you make error handling compulsory and yet the user insists on ignoring error, they can still do that. However, they have to be very explicit about it (to prevent accidental ignore). For instance:
func UpperFunction (ctx ErrorContext) {
تم التجاهل: = سياق جديد ()
وظيفتي (تم تجاهله) // تم تجاهل هذه الوظيفة

 MyFunction(ctx) //this one is handled

}
""
هذا النهج لا يغير أي شيء في اللغة الحالية.

ALTree Alberto ، ماذا عن خلط check وماذا اقترح ianlancetaylor ؟

لذا

func F() (int, string, error) {
   i, s, err := OhNo()
   if err != nil {
      return i, s, &BadStuffHappened(err, "oopsie-daisy")
   }
   // all is good
   return i+1, s+" ok", nil
}

يصبح

func F() (int, string, error) {
   i, s, err := OhNo()
   check i, s, err || &BadStuffHappened(err, "oopsie-daisy")
   // all is good
   return i+1, s+" ok", nil
}

أيضًا ، يمكننا تقييد check للتعامل مع أنواع الأخطاء فقط ، لذلك إذا كنت بحاجة إلى قيم إرجاع متعددة ، فيجب تسميتها وتعيينها ، لذلك فهي تقوم بتعيين "in-place" بطريقة ما وتتصرف مثل "إرجاع" بسيط

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check err |=  &BadStuffHappened(err, "oopsy-daisy")  // assigns in place and behaves like simple "return"
   // all is good
   return i+1, s+" ok", nil
}

إذا أصبح return مقبولاً في التعبير يومًا ما ، فلن تكون هناك حاجة إلى check ، أو تصبح دالة قياسية

func check(e error) bool {
   return e != nil
}

func F() (a int, s string, err error) {
   i, s, err = OhNo()
   check(err) || return &BadStuffHappened(err, "oopsy-daisy")
   // all is good
   return i+1, s+" ok", nil
}

الحل الأخير يبدو وكأنه بيرل على الرغم من 😄

لا أستطيع أن أتذكر من اقترحها في الأصل ، ولكن إليك فكرة أخرى في بناء الجملة (الدراجة المفضلة للجميع :-). أنا لا أقول إنها فكرة جيدة ، لكن إذا أردنا طرح الأفكار في الوعاء ...

x, y := try foo()

سيكون معادلاً لـ:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), err
}

و

x, y := try foo() catch &FooErr{E:$, S:"bad"}

سيكون معادلاً لـ:

x, y, err := foo()
if err != nil {
    return (an appropriate number of zero values), &FooErr{E:err, S:"bad"}
}

من المؤكد أن النموذج try تم اقتراحه من قبل ، عدة مرات ، اختلافات في بناء الجملة السطحية. غالبًا ما يتم اقتراح نموذج try ... catch ، لكن من الواضح أنه مشابه لـ ALTree 's check A, B build واقتراح المتابعة الخاص بـ

z(try foo() catch &FooErr{E:$, S:"bad"})

يمكن أن يكون لديك عدة محاولات / عمليات التقاط في بيان واحد:

p = try q(0) + try q(1)
a = try b(c, d() + try e(), f, try g() catch &GErr{E:$}, h()) catch $BErr{E:$}

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

من الواضح أن الكلمات الرئيسية الجديدة مثل try و catch ستؤدي إلى كسر توافق Go 1.x.

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

if err != nil {
    return err
}

أو تقليل عدد المرات للتحقق من الخطأ؟ قد يكون من محاولة / التقاط الحل لهذا الغرض.

أود أن أقترح أن أي صيغة اختصار معقولة لمعالجة الأخطاء لها ثلاث خصائص:

  1. يجب ألا يظهر قبل الرمز الذي يتحقق منه ، بحيث يكون المسار غير الخاطئ بارزًا.
  2. لا ينبغي أن يقدم متغيرات ضمنية في النطاق ، حتى لا يختلط الأمر على القراء عندما يكون هناك متغير صريح يحمل نفس الاسم.
  3. لا ينبغي أن يجعل إجراء استرداد واحد (على سبيل المثال ، return err ) أسهل من الآخر. في بعض الأحيان ، قد يكون من الأفضل اتخاذ إجراء مختلف تمامًا (مثل استدعاء t.Fatal ). لا نريد أيضًا ثني الأشخاص عن إضافة سياق إضافي.

بالنظر إلى هذه القيود ، يبدو أن بنية بسيطة واحدة تقريبًا ستكون شيئًا من هذا القبيل

STMT SEPARATOR_TOKEN VAR BLOCK

فمثلا،

syscall.Chdir(dir) :: err { return err }

وهو ما يعادل

if err := syscall.Chdir(dir); err != nil {
    return err
}
````
Even though it's not much shorter, the new syntax moves the error path out of the way. Part of the change would be to modify `gofmt` so it doesn't line-break one-line error-handling blocks, and it indents multi-line error-handling blocks past the opening `}`.

We could make it a bit shorter by declaring the error variable in place with a special marker, like

syscall.Chdir (dir) :: { returnerr }
""

أتساءل كيف يتصرف هذا بالنسبة للقيمة غير الصفرية والخطأ اللذين تم إرجاعهما. على سبيل المثال ، ربما يُرجع bufio.Peek قيمة غير صفرية و ErrBufferFull في نفس الوقت.

mattn لا يزال بإمكانك استخدام الصيغة القديمة.

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

ret, err := doSomething() :: err { return err }
return ret, err

jba ما تصفه يبدو لمعامِل تكوين وظيفة منقولة:

syscall.Chdir(dir) ⫱ func (err error) { return &PathError{"chdir", dir, err} }

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

لذا أفكر الآن في ثلاث ملاحظات مرتبطة ببعضها البعض:

  1. التعامل مع الأخطاء يشبه تكوين الوظيفة ، ولكن الطريقة التي نؤدي بها الأشياء في Go هي نوع من عكس Haskell's Error monad : نظرًا لأننا نكتب في الغالب أمرًا بدلاً من رمز متسلسل ، فنحن نريد تحويل الخطأ (لإضافة سياق) بدلاً من القيمة غير الخاطئة (التي نفضل ربطها بمتغير).

  2. وظائف Go التي تُرجع (x, y, error) عادةً ما تعني شيئًا أشبه باتحاد (# 19412) (x, y) | error .

  3. في اللغات التي تفكك النقابات أو تطابق النمط ، تكون الحالات نطاقات منفصلة ، والكثير من المشاكل التي نواجهها مع أخطاء في Go ترجع إلى التظليل غير المتوقع للمتغيرات المعاد تعريفها والتي يمكن تحسينها بفصل تلك النطاقات (# 21114).

لذلك ربما ما نريده حقًا هو مثل عامل التشغيل =: ، ولكن بنوع من شرط مطابقة الاتحاد:

syscall.Chdir(dir) =? err { return &PathError{"chdir", dir, err} }

"اذهب
n: = io.WriteString (w، s) =؟ يخطئ {إرجاع الخطأ}

and perhaps a boolean version for `, ok` index expressions and type assertions:
```go
y := m[x] =! { return ErrNotFound }

باستثناء تحديد النطاق ، لا يختلف هذا كثيرًا عن مجرد تغيير gofmt ليكون أكثر قابلية للتكيف مع الخطوط الفردية:

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} }

"اذهب
n ، يخطئ: = io.WriteString (w ، s) ؛ إذا أخطأ! = لا شيء {إرجاع خطأ}

```go
y, ok := m[x]; if !ok { return ErrNotFound }

لكن تحديد النطاق مشكلة كبيرة! مشاكل تحديد النطاق هي حيث يعبر هذا النوع من التعليمات البرمجية الخط من "القبيح إلى حد ما" إلى "الأخطاء الدقيقة".

تضمين التغريدة
بينما أنا من المعجبين بالفكرة الشاملة ، فأنا لست مؤيدًا كبيرًا للصيغة المشفرة التي تشبه بيرل لها. ربما يكون بناء الجملة الأكثر لفظًا أقل إرباكًا ، مثل:

syscall.Chdir(dir) or dump(err): errors.Wrap(err, "chdir failed")

syscall.Chdir(dir) or dump

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

resp := http.Get("https://example.com") or dump

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

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

هذا وحده يجعل هذا الاقتراح لا يستحق كل هذا العناء IMHO.

أنا شخصياً أفضل بناء الجملة هذا

err := syscall.Chdir(dir)
if err != nil {
    return err
}
return nil

على

if err := syscall.Chdir(dir); err != nil {
    return err
}
return nil

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

bcmills :

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

ليس فقط النطاق ؛ هناك أيضا الحافة اليسرى. أعتقد أن هذا يؤثر حقًا على قابلية القراءة. أعتقد

syscall.Chdir(dir) =: err; if err != nil { return &PathError{"chdir", dir, err} } 

أوضح بكثير من

err := syscall.Chdir(dir); if err != nil { return &PathError{"chdir", dir, err} } 

خاصة عندما يحدث ذلك على عدة خطوط متتالية ، لأن عينك يمكنها مسح الحافة اليسرى لتجاهل معالجة الأخطاء.

مزج فكرة bcmills يمكننا تقديم مشغل شحن الأنابيب المشروط.

سيتم تنفيذ الوظيفة F2 إذا كانت القيمة الأخيرة ليست صفرية .

func F1() (foo, bar){}

first := F1() ?> last: F2(first, last)

حالة خاصة لإعادة توجيه الأنابيب مع بيان الإرجاع

func Chdir(dir string) error {
    syscall.Chdir(dir) ?> err: return &PathError{"chdir", dir, err}
    return nil
}

مثال حقيقي قدمته urandom في عدد آخر
بالنسبة لي أكثر قابلية للقراءة مع التركيز على التدفق الأساسي

func configureCloudinit(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig, error) {
    // When bootstrapping, we only want to apt-get update/upgrade
    // and setup the SSH keys. The rest we leave to cloudinit/sshinit.
    udata := cloudconfig.NewUserdataConfig(icfg, cloudcfg) ?> err: return nil, err
    if icfg.Bootstrap != nil {
        udata.ConfigureBasic() ?> err: return nil, err
        return udata, nil
    }
    udata.Configure() ?> err: return nil, err
    return udata, nil
}

func ComposeUserData(icfg *instancecfg.InstanceConfig, cloudcfg cloudinit.CloudConfig, renderer renderers.ProviderRenderer) ([]byte, error) {
    if cloudcfg == nil {
        cloudcfg = cloudinit.New(icfg.Series) ?> err: return nil, errors.Trace(err)
    }
    _ = configureCloudinit(icfg, cloudcfg) ?> err: return nil, errors.Trace(err)
    operatingSystem := series.GetOSFromSeries(icfg.Series) ?> err: return nil, errors.Trace(err)
    udata := renderer.Render(cloudcfg, operatingSystem) ?> err: return nil, errors.Trace(err)
    logger.Tracef("Generated cloud init:\n%s", string(udata))
    return udata, nil
}

أوافق على معالجة الخطأ ليست مريحة. أي عندما تقرأ الكود أدناه ، يجب عليك نطقه بـ if error not nil then -التي تترجم إلى if there is an error then .

if err != nil {
    // handle error
}

أود أن يكون لدي القدرة على expresing الرمز أعلاه بهذه الطريقة - والتي في رأيي أكثر قابلية للقراءة.

if err {
    // handle error
}

فقط اقتراحي المتواضع :)

يبدو مثل بيرل ، بل إنه يحتوي على المتغير السحري
كمرجع ، في بيرل ستفعل

افتح (FILE، $ file) أو يموت ("لا يمكن فتح $ file: $!")؛

IMHO ، لا يستحق كل هذا العناء ، إحدى النقاط التي أحبها في go هي معالجة الأخطاء
صريح و "في وجهك"

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

e: = syscall.Chdir (dir)؟> e: & PathError {"chdir"، dir، e}

وقد نستخدم أيضًا رمزًا مختلفًا عن || محددة لهذه المهمة ،
أعتقد أن الرموز النصية مثل "أو" غير ممكنة بسبب التراجع
التوافق

n، _، err، _ = somecall (...)؟> يخطئ: & PathError {"somecall"، n، err}

في الثلاثاء ، 1 آب (أغسطس) 2017 ، الساعة 2:47 مساءً ، كتب Rodrigo [email protected] :

يمكننا تقديم فكرة خلط bcmills https://github.com/bcmills
مشغل شحن الأنابيب المشروط.

سيتم تنفيذ الدالة F2 إذا كانت القيمة الأخيرة ليست صفرية .

func F1 () (foo، bar) {}
أولاً: = F1 ()؟> أخيرًا: F2 (أولًا ، أخيرًا)

حالة خاصة لإعادة توجيه الأنابيب مع بيان الإرجاع

func Chdir (سلسلة dir) خطأ {
syscall.Chdir (dir)؟> err: return & PathError {"chdir"، dir، err}
العودة لا شيء
}

مثال حقيقي
https://github.com/juju/juju/blob/01b24551ecdf20921cf620b844ef6c2948fcc9f8/cloudconfig/providerinit/providerinit.go
جلبتها urandom https://github.com/urandom في قضية أخرى
بالنسبة لي أكثر قابلية للقراءة مع التركيز على التدفق الأساسي

func configCloudinit (icfg * instancecfg.InstanceConfig ، cloudcfg cloudinit.CloudConfig) (cloudconfig.UserdataConfig ، خطأ) {
// عند التمهيد ، نريد فقط apt-get update / Upgrade
// وإعداد مفاتيح SSH. الباقي نتركه إلى cloudinit / sshinit.
udata: = cloudconfig.NewUserdataConfig (icfg، cloudcfg)؟> يخطئ: إرجاع لا شيء ، يخطئ
إذا كان icfg.Bootstrap! = لا شيء {
udata.ConfigureBasic ()؟> err: إرجاع لا شيء ، يخطئ
عودة udata ، لا شيء
}
udata.Configure ()؟> err: إرجاع لا شيء ، يخطئ
عودة udata ، لا شيء
}
func ComposeUserData (icfg * instancecfg.InstanceConfig، cloudcfg cloudinit.CloudConfig، renderer renderers.ProviderRenderer) ([] بايت ، خطأ) {
إذا كانت cloudcfg == لا شيء {
cloudcfg = cloudinit.New (icfg.Series)؟> يخطئ: إرجاع لا شيء ، أخطاء. تتبع (يخطئ)
}
configCloudinit (icfg، cloudcfg)؟> err: إرجاع لا شيء ، أخطاء. تتبع (يخطئ)
OperatingSystem: = series.GetOSFromSeries (icfg.Series)؟> يخطئ: إرجاع لا شيء ، أخطاء. تتبع (يخطئ)
udata: = Renderer.Render (cloudcfg، OperatingSystem)؟> err: return nil، errors.Trace (err)
logger.Tracef ("تهيئة السحابة المُنشأة: \ n٪ s" ، سلسلة (udata))
عودة udata ، لا شيء
}

-
أنت تتلقى هذا لأنك مشترك في هذا الموضوع.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/golang/go/issues/21161#issuecomment-319359614 ، أو كتم الصوت
الخيط
https://github.com/notifications/unsubscribe-auth/AbwRO_J0h2dQHqfysf2roA866vFN4_1Jks5sTx5hgaJpZM4Oi1c-
.

هل أنا الوحيد الذي يعتقد أن كل هذه التغييرات المقترحة ستكون أكثر تعقيدًا من الشكل الحالي.

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

rodcorsi أعلم أنه يبدو بسيطًا ، لكنني أعتقد أنه من المهم بالنسبة للجزء الثاني أن يكون كتلة : تستخدم العبارات الحالية if و for الكتل ، و select و يستخدم كل من switch بناء جملة محددًا بأقواس متعرجة ، لذلك يبدو من الصعب حذف الأقواس لعملية التحكم في التدفق هذه.

من الأسهل أيضًا التأكد من أن شجرة التحليل لا لبس فيها إذا لم تكن مضطرًا للقلق بشأن التعبيرات العشوائية التي تتبع الرموز الجديدة.

التركيب اللغوي والدلالات التي كنت أفكر في رسمها هي:


NonZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                     ExpressionList assign_op Expression ) "=?" [ identifier ] Block .
ZeroGuardStmt = ( Expression | IdentifierList ":=" Expression |
                  ExpressionList assign_op Expression ) "=!" Block .

يتم تنفيذ NonZeroGuardStmt Block إذا كانت القيمة الأخيرة لـ Expression لا تساوي القيمة الصفرية لنوعها. إذا كان identifier موجودًا ، فإنه يرتبط بهذه القيمة ضمن Block . يتم تنفيذ ZeroGuardStmt Block إذا كانت القيمة الأخيرة لـ Expression تساوي القيمة الصفرية لنوعها.

بالنسبة للنموذج := ، فإن القيم الأخرى (الرائدة) لـ Expression مرتبطة بـ IdentifierList كما في ShortVarDecl . يتم الإعلان عن المعرفات في النطاق المتضمن ، مما يعني أنها مرئية أيضًا داخل Block .

بالنسبة للنموذج assign_op ، يجب أن يكون كل معامل في الجانب الأيسر قابلاً للعنونة ، أو تعبير فهرس الخريطة ، أو (لتعيينات = فقط) معرف فارغ. قد تكون المعاملات بين قوسين. يتم تقييم القيم الأخرى (البادئة) للجانب الأيمن Expression كما في Assignment . تحدث المهمة قبل تنفيذ Block وبغض النظر عما إذا كان Block تم تنفيذه أم لا.


أعتقد أن القواعد النحوية المقترحة هنا متوافقة مع Go 1: ? ليس معرفًا صالحًا ولا توجد عوامل تشغيل Go حالية تستخدم هذا الحرف ، وعلى الرغم من أن ! عامل صالح ، فلا يوجد الإنتاج الحالي الذي يمكن أن يتبعه { .

bcmills LGTM ، مع التغييرات المصاحبة للحكومة.

كنت أعتقد أنك ستجني =? و =! لكل رمز مميز بحد ذاته ، مما يجعل القواعد متوافقة بشكل تافه.

كنت أعتقد أنك ستجعل =؟ و =! كل رمز في حد ذاته ، مما يجعل القواعد متوافقة بشكل تافه.

يمكننا فعل ذلك في القواعد ، ولكن ليس في المعجم: التسلسل "=!" يمكن أن تظهر في كود Go 1 الصحيح (https://play.golang.org/p/pMTtUWgBN9).

القوس المتعرج هو ما يجعل التحليل لا لبس فيه في اقتراحي: =! يمكن أن يظهر حاليًا فقط في إعلان أو تعيين لمتغير منطقي ، ولا يمكن أن تظهر الإعلانات والتعيينات حاليًا مباشرة قبل قوس مجعد (https : //play.golang.org/p/ncJyg-GMuL) ما لم يتم الفصل بينها بفاصلة منقوطة ضمنية (https://play.golang.org/p/lhcqBhr7Te).

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

يعجبني اقتراح check لأنه يمكنك أيضًا تمديده للتعامل معه

f, err := os.Open(myfile)
check err
defer check f.Close()

لا يبدو أن المقترحات الأخرى يمكن أن تختلط مع defer أيضًا. check أيضًا سهل القراءة جدًا ، وبسيط لـ Google إذا كنت لا تعرفه. لا أعتقد أنه يجب أن يقتصر على النوع error . أي شيء يمثل معامل إرجاع في الموضع الأخير يمكنه استخدامه. لذلك ، قد يكون للمكرر check مقابل Next() bool .

لقد كتبت ذات مرة ماسح ضوئي يشبه

func (s *Scanner) Next() bool {
    if s.Error != nil || s.pos >= s.RecordCount {
        return false
    }
    s.pos++

    var rt uint8
    if !s.read(&rt) {
        return false
    }
...

قد يكون هذا الجزء الأخير check s.read(&rt) بدلاً من ذلك.

تضمين التغريدة

لا يبدو أن المقترحات الأخرى يمكن أن تختلط مع defer أيضًا.

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

defer f.Close() =? err { return err }

نظرًا لأن اقتراحALTree check يقدم بيانًا منفصلاً ، فأنا لا أرى كيف يمكنك مزج ذلك مع defer الذي يفعل أي شيء بخلاف مجرد إرجاع الخطأ.

defer func() {
  err := f.Close()
  check err, fmt.Errorf(…, err) // But this func() doesn't return an error!
}()

مقابلة:

defer f.Close() =? err { return fmt.Errorf(…, err) }

المبرر للعديد من هذه المقترحات هو "بيئة العمل" الأفضل ولكني لا أرى حقًا كيف أن أيًا منها أفضل من جعلها ، لذلك هناك القليل من الكتابة. كيف تزيد هذه من قابلية صيانة الكود؟ التوافق؟ المقروئية؟ سهولة فهم تدفق التحكم؟

تضمين التغريدة

كيف تزيد هذه من قابلية صيانة الكود؟ التوافق؟ المقروئية؟ سهولة فهم تدفق التحكم؟

كما أشرت سابقًا ، من المحتمل أن تأتي الميزة الرئيسية لأي من هذه المقترحات من تحديد نطاق أوضح للمهام ومتغيرات err : راجع # 19727 ، # 20148 ، # 5634 ، # 21114 ، وربما غيرها من المتغيرات المتنوعة الطرق التي يواجه بها الأشخاص مشكلات تحديد النطاق فيما يتعلق بمعالجة الأخطاء.

bcmills شكرًا على توفير الحافز وآسف أنني فاتني ذلك في

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

أتذكر عندما تم تقديم السلوك الحالي لـ := - الكثير من هذا الموضوع أثناء التنقل † كان صراخًا للحصول على طريقة لتوضيح الأسماء التي سيتم إعادة استخدامها بشكل صريح بدلاً من "إعادة الاستخدام الضمني فقط إذا كان هذا المتغير موجودًا في النطاق الحالي بالضبط "حيث تظهر جميع المشكلات الدقيقة التي يصعب رؤيتها ، في تجربتي.

† لا أستطيع أن أجد هذا الموضوع هل لدى أي شخص رابط؟

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

تضمين التغريدة

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

نعم. هذا أحد الأشياء التي أحبها في عامل التشغيل =? أو :: الذي اقترحه أنا و

أنا شخصياً أظن أنني سأكون أكثر سعادة على المدى الطويل بميزة نوع بيانات جبرية ذات علامات أكثر وضوحًا (انظر أيضًا # 19412) ، لكن هذا تغيير أكبر بكثير في اللغة: من الصعب أن نرى كيف يمكننا التعديل التحديثي ذلك على واجهات برمجة التطبيقات الموجودة في بيئة Go 1 / Go 2 المختلطة.

سهولة فهم تدفق التحكم؟

في مقترحاتي و لعينك مسح الجانب الأيسر لأسفل

bcmills أعتقد أنني مسؤول عن ما لا يقل عن نصف الكلمات الموجودة في # 19412 ، لذلك لا تحتاج إلى بيعي على أنواع المجموع ؛)

عندما يتعلق الأمر بإعادة الأشياء مع وجود خطأ ، فهناك أربع حالات

  1. مجرد خطأ (لا تحتاج إلى فعل أي شيء ، فقط قم بإرجاع خطأ)
  2. أشياء وخطأ (ستتعامل مع ذلك تمامًا كما تفعل الآن)
  3. شيء واحد أو خطأ (يمكنك استخدام أنواع الجمع!: tada :)
  4. شيئين أو أكثر أو خطأ

إذا ضربت الرقم 4 ، فهذا هو المكان الذي تصبح فيه الأمور صعبة. بدون تقديم أنواع tuple (أنواع المنتجات غير المصنفة لتتماشى مع أنواع المنتجات المصنفة في البنية) ، سيتعين عليك تقليل المشكلة إلى الحالة 3 عن طريق تجميع كل شيء في بنية إذا كنت تريد استخدام أنواع الجمع لنمذجة "هذا أو خطأ".

قد يتسبب تقديم أنواع tuple في جميع أنواع المشكلات ومشكلات التوافق والتداخلات الغريبة (هل func() (int, string, error) عبارة عن مجموعة معرفة ضمنيًا أم أن قيم الإرجاع المتعددة هي مفهوم منفصل؟ إذا كانت بنية tuple محددة ضمنيًا ، فهل هذا يعني func() (n int, msg string, err error) هي بنية محددة ضمنيًا !؟ إذا كانت هذه بنية ، فكيف يمكنني الوصول إلى الحقول إذا لم أكن في نفس الحزمة!)

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

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

بدون تقديم أنواع tuple [...] ، سيتعين عليك [تجميع] كل شيء في بنية إذا كنت تريد استخدام أنواع الجمع لنمذجة "هذا أو خطأ".

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

المشكلة الكبرى هي الترحيل: كيف يمكننا الانتقال من نموذج "القيم والخطأ" في Go 1 إلى نموذج "قيم أو خطأ" محتمل في Go 2 ، خاصةً مع وجود واجهات برمجة تطبيقات مثل io.Writer التي تُرجع فعلاً "القيم والخطأ

ما زلت أعتقد أن أنواع المجموع توفر العديد من الفوائد ، لكنها لا تفعل أي شيء لإصلاح مشكلات تحديد النطاق ، بالطبع.

هذا يعتمد على كيفية تفريغها ، والذي أفترض أنه يعيدنا إلى ما نحن عليه اليوم. إذا كنت تفضل النقابات ، فربما يمكنك تخيل إصدار =? كواجهة برمجة تطبيقات "غير متماثلة لمطابقة الأنماط":

i := match strconv.Atoi(str) | err error { return err }

حيث match ستكون عملية مطابقة النمط التقليدية بنمط ML ، ولكن في حالة المطابقة غير الشاملة ستعيد القيمة (مثل interface{} إذا كان لدى الاتحاد أكثر من بديل واحد لا مثيل له) بدلا من الذعر مع فشل المطابقة غير الشامل.

لقد تحققت للتو في حزمة على https://github.com/mpvl/errd تتناول المشكلات التي تمت مناقشتها هنا برمجيًا (لا توجد تغييرات في اللغة). أهم جانب في هذه الحزمة هو أنها لا تقصر معالجة الأخطاء فحسب ، بل تسهل أيضًا القيام بذلك بشكل صحيح. أعطي أمثلة في المستندات حول كيف أن معالجة الأخطاء الاصطلاحية التقليدية أكثر صعوبة مما يبدو ، خاصة في التفاعل مع التأجيل.

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

لا تزال تعمل على بعض الأمثلة الأخرى ، ولكن هذه الحزمة جاهزة للتجربة.

bcmills مليون: +1: لـ # 12854

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

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

اذهب ليس كبيرًا على السكر أو السحر وهذه ميزة إضافية.

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

إذا حصلت Go 2 على أنواع المجموع - والتي من شأنها أن تصدمني بصراحة (بطريقة جيدة!) - فستكون ، إن وجدت ، عملية تدريجية بطيئة للغاية للانتقال إلى "النمط الجديد" وفي هذه الأثناء سيكون هناك المزيد من التشتت والارتباك في كيفية التعامل مع الأخطاء ، لذلك لا أرى أنها نتيجة إيجابية. (ومع ذلك ، سأبدأ على الفور في استخدامه لأشياء مثل chan union { Msg1 T; Msg2 S; Err error } بدلاً من ثلاث قنوات).

إذا كان هذا قبل Go1 ويمكن لفريق Go أن يقول "سنقضي الأشهر الستة المقبلة في نقل كل شيء ، وعندما يكسر الأشياء ، تابع ذلك" فسيكون ذلك شيئًا واحدًا ، لكننا الآن عالقون بشكل أساسي حتى لو حصلنا على أنواع المجموع.

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

كما حاولت أن أقول أعلاه ، لا أعتقد أنه من الضروري للطريقة الجديدة ، مهما كانت ، لتغطية "إرجاع X والخطأ". إذا كانت الغالبية العظمى من الحالات هي "إرجاع X أو خطأ" ، والطريقة الجديدة تعمل على تحسين ذلك فقط ، فهذا رائع ، ولا يزال بإمكانك استخدام الطريقة القديمة المتوافقة مع Go 1 للأندر "إرجاع X والخطأ".

nigeltao صحيح ، لكننا ما زلنا بحاجة إلى طريقة ما

jimmyfrasche لا أعتقد أنه يمكنني بناء حجة لذلك. يمكنك مشاهدة حديثي أو الاطلاع على المثال في الريبو README . ولكن إذا لم يكن الدليل المرئي مقنعًا لك ، فلا يوجد شيء يمكنني قوله.

jba شاهد الحديث وقراءة التمهيدي. أفهم الآن من أين أتيت مع الشيء الوراثي / الحاشية السفلية / التعليق الختامي / sidenote (وأنا من محبي sidenotes (والأبوية)).

إذا كان الهدف هو وضع المسار غير السعيد إلى الجانب بسبب عدم وجود مصطلح أفضل ، فعندئذٍ سيعمل المكون الإضافي $ EDITOR بدون تغيير اللغة وسيعمل مع جميع التعليمات البرمجية الموجودة ، بغض النظر عن تفضيلات مؤلف الكود.

يؤدي تغيير اللغة إلى جعل بناء الجملة أكثر إحكاما إلى حد ما. يذكر bcmills أن هذا يحسن تحديد النطاق ولكني لا أرى حقًا كيف يمكن ذلك ما لم يكن لديه قواعد نطاق مختلفة عن := ولكن يبدو أن هذا قد يسبب مزيدًا من الارتباك.

bcmills أنا لا أفهم تعليقك. من الواضح أنه يمكنك التمييز بينهما. الطريقة القديمة تبدو كالتالي:

err := foo()
if err != nil {
  return n, err  // n can be non-zero
}

الطريقة الجديدة تبدو

check foo()

أو

foo() || &FooError{err}

أو أيا كان لون الدراجة. أعتقد أن معظم المكتبات القياسية يمكن أن تنتقل ، ولكن ليس كل منها مضطر إلى ذلك.

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

ضع في اعتبارك على سبيل المثال ، الكتابة إلى ملف Google Cloud Storage ، حيث نريد إحباط كتابة الملف عند حدوث أي خطأ:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer func() {
        if r := recover(); r != nil {
            w.CloseWithError(fmt.Errorf("panic: %v", r))
            panic(r)
        }
        if err != nil {
            _ = w.CloseWithError(err)
        } else {
            err = w.Close()
        }
    }
    _, err = io.Copy(w, r)
    return err
}

تشمل التفاصيل الدقيقة لهذا الرمز ما يلي:

  • يتم تمرير الخطأ من Copy بشكل خفي عبر وسيطة الإرجاع المسماة إلى وظيفة defer.
  • لنكون آمنين تمامًا ، نصاب بالذعر من r ونتأكد من إجهاضنا الكتابة قبل استئناف الذعر.
  • إن تجاهل خطأ الإغلاق الأول هو أمر مقصود ، لكن كيندا يبدو وكأنه قطعة أثرية للمبرمج الكسول.

باستخدام Package Errd ، يبدو هذا الرمز كما يلي:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client, err := storage.NewClient(ctx)
        e.Must(err)
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _, err = io.Copy(w, r)
        e.Must(err)
    })
}

errd.Discard هو معالج أخطاء. يمكن أيضًا استخدام معالجات الأخطاء للالتفاف والتسجيل وأي أخطاء.

e.Must ما يعادل foo() || wrapError

e.Defer إضافي ويتعامل مع تمرير الأخطاء إلى المؤجِّلين.

باستخدام الأدوية الجنيسة ، يمكن أن يبدو هذا الجزء من الكود مشابهًا لما يلي:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.Must(storage.NewClient(ctx))
        e.Defer(client.Close, errd.Discard)

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        e.Defer(w.CloseWithError)

        _ = e.Must(io.Copy(w, r))
    })
}

إذا قمنا بتوحيد الطرق المستخدمة في Defer ، فقد يبدو الأمر كما يلي:

func writeToGS(ctx context.Context, bucket, dst, src string, r io.Reader) error {
    return errd.Run(func(e *errd.E) {
        client := e.DeferClose(e.Must(storage.NewClient(ctx)), errd.Discard)
       e.Must(io.Copy(e.DeferClose(client.Bucket(bucket).Object(dst).NewWriter(ctx)), r)
    })
}

حيث يختار DeferClose إما Close أو CloseWithError. عدم قول هذا أفضل ، ولكن مجرد إظهار الاحتمالات.

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

يجب أن يركز الحل الذي يحسن الأخطاء على الأقل على جعل الأمور في نصابها أسهل من التركيز على جعل الأمور أقصر.

ALTree Errd يعالج "استعادة الأخطاء المعقدة" خارج الصندوق.

jimmyfrasche : Errd يفعل تقريبًا ما يفعله مثال الملعب الخاص بك ، ولكنه ينسج أيضًا في تمرير الأخطاء والذعر للإذعان.

jimmyfrasche : أوافق على أن معظم المقترحات لا تضيف الكثير لما يمكن تحقيقه بالفعل في الكود.

romainmenke : توافق على أن هناك الكثير من التركيز على الإيجاز. لتسهيل القيام بالأشياء بشكل صحيح ، يجب التركيز بشكل أكبر.

jba : نهج errd يجعل من السهل إلى حد ما مسح الخطأ مقابل التدفق غير الخطأ بمجرد النظر إلى الجانب الأيسر (أي شيء يبدأ بـ e. هو خطأ أو تأجيل معالجة). كما أنه يجعل من السهل جدًا مسح أي من القيم المرتجعة يتم التعامل معها بحثًا عن خطأ أو تأجيل وأيها لا يتم التعامل معه.

bcmills : على الرغم من أن errd لا يصلح مشكلات النطاق في حد ذاته ، إلا أنه يلغي الحاجة إلى تمرير أخطاء المصب إلى متغيرات الخطأ المعلنة سابقًا وجميعها ، وبالتالي تخفيف المشكلة بشكل كبير لمعالجة الأخطاء ، AFAICT.

Erdd يبدو أنه يعتمد كليًا على حالات الذعر والانتعاش. يبدو أنه يأتي مع عقوبة أداء كبيرة. لست متأكدًا من أنه حل شامل بسبب هذا.

urandom : يتم تنفيذه تحت غطاء محرك السيارة باعتباره أكثر تكلفة ، ولكن إرجاء واحد.
إذا كان الكود الأصلي:

  • لا يوجد تأجيل: عقوبة استخدام errd كبيرة ، حوالي 100ns *.
  • يستخدم التأجيل الاصطلاحي: يكون وقت التشغيل أو الخطأ من نفس الترتيب ، على الرغم من أنه أبطأ إلى حد ما
  • يستخدم معالجة الأخطاء المناسبة للتأجيل: وقت التشغيل متساوٍ تقريبًا ؛ يمكن أن يكون errd أسرع إذا كان عدد المؤجِّلات> 1

النفقات العامة الأخرى:

  • يؤدي تمرير الإغلاق (w.Close) إلى Defer حاليًا أيضًا إلى إضافة 25ns * النفقات العامة ، مقارنة باستخدام DeferClose أو DeferFunc API (انظر الإصدار v0.1.0). بعد المناقشة مع rsc ، قمت بإزالته للحفاظ على واجهة برمجة التطبيقات بسيطة والقلق بشأن التحسين لاحقًا.
  • التفاف سلاسل الخطأ المضمنة كمعالجات ( e.Must(err, msg("oh noes!") ) يكلف حوالي 30 نانوثانية مع Go 1.8. مع الطرف (1.9) ، على الرغم من أنه لا يزال تخصيصًا ، فقد سجلت التكلفة عند 2ns. بالطبع بالنسبة لرسائل الخطأ المُعلنة مسبقًا ، لا تزال التكلفة ضئيلة.

(*) جميع الأرقام التي تعمل على جهاز MacBook Pro الخاص بي 2016.

الكل في الكل ، تبدو التكلفة مقبولة إذا كانت التعليمات البرمجية الأصلية تستخدم التأجيل. إذا لم يكن الأمر كذلك ، فإن أوستن تعمل على تقليل تكلفة التأجيل بشكل كبير ، وبالتالي قد تنخفض التكلفة بمرور الوقت.

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

jimmyfrasche :

عندها سيعمل المكون الإضافي $ EDITOR بدون تغيير اللغة

نعم ، هذا بالضبط ما أجادله في الحديث. أنا هنا أزعم أننا إذا أجرينا تغييرًا في اللغة ، فيجب أن يتوافق مع مفهوم "sidenote".

تضمين التغريدة

من الواضح أنه يمكنك التمييز بينهما. الطريقة القديمة تبدو كالتالي:

أنا أتحدث عن نقطة الإعلان ، وليس نقطة الاستخدام.

بعض الاقتراحات التي تمت مناقشتها هنا لا تميز بين الاثنين في موقع الاتصال ، لكن البعض الآخر يفعل ذلك. إذا اخترنا أحد الخيارات التي تفترض "القيمة أو الخطأ" - مثل || أو try … catch أو match - إذن يجب أن يكون خطأ وقت التجميع للاستخدام ذلك التركيب مع وظيفة "قيمة والخطأ"، وينبغي أن تصل إلى منفذة من وظيفة لتحديد أي من ذلك.

في وقت الإعلان ، لا توجد حاليًا طريقة للتمييز بين "القيمة والخطأ" و "القيمة أو الخطأ":

func Atoi(string) (int, error)

و

func WriteString(Writer, String) (int, error)

لها نفس أنواع الإرجاع ، لكن لها دلالات خطأ مختلفة.

mpvl أنا أبحث في المستندات و src لـ errd. أعتقد أنني بدأت أفهم كيف يعمل ، لكن يبدو أنه يحتوي على الكثير من واجهة برمجة التطبيقات التي تعترض طريق الفهم الذي يبدو كما لو كان يمكن تنفيذه في حزمة منفصلة. أنا متأكد من أن كل ذلك يجعله أكثر فائدة من الناحية العملية ولكن كتوضيح فإنه يضيف الكثير من الضوضاء.

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

type Handler = func(ctx context.Context, panicing bool, err error) error
Run(context.Context, func(*E), defaults ...Handler) //egregious style but most minimal
type struct E {...}
func (*E) Must(err error, handlers ...Handler)
func (*E) Defer(func() error, handlers ...Handler)

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

تضمين التغريدة

يذكر bcmills أن هذا يعمل على تحسين النطاق ولكني لا أرى حقًا كيف يمكن ذلك

التحسين الرئيسي هو إبقاء المتغير err خارج النطاق. سيؤدي ذلك إلى تجنب الأخطاء مثل تلك المرتبطة من https://github.com/golang/go/issues/19727. لتوضيح مقتطف من أحدهم:

    res, err := ctxhttp.Get(ctx, c.HTTPClient, dirURL)
    if err != nil {
        return Directory{}, err
    }
    defer res.Body.Close()
    c.addNonce(res.Header)
    if res.StatusCode != http.StatusOK {
        return Directory{}, responseError(res)
    }

    var v struct {
        …
    }
    if json.NewDecoder(res.Body).Decode(&v); err != nil {
        return Directory{}, err
    }

يحدث الخطأ في جملة if-statement الأخيرة: تم إسقاط الخطأ من Decode ، لكنه ليس واضحًا لأن err من فحص سابق لا يزال في النطاق. في المقابل ، باستخدام عامل التشغيل :: أو =? ، سيتم كتابته:

    res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err }
    defer res.Body.Close()
    c.addNonce(res.Header)
    (res.StatusCode == http.StatusOK) =! { return Directory{}, responseError(res) }

    var v struct {
        …
    }
    json.NewDecoder(res.Body).Decode(&v) =? err { return Directory{}, err }

يوجد هنا نوعان من تحسينات النطاق التي تساعد:

  1. أول err (من المكالمة السابقة Get ) فقط في نطاق المجموعة return ، لذلك لا يمكن استخدامها عن طريق الخطأ في الشيكات اللاحقة.
  2. نظرًا لأن err من Decode تم التصريح عنه في نفس البيان الذي يتم فيه التحقق من عدم وجود خطأ ، فلا يمكن أن يكون هناك انحراف بين الإقرار والشيك.

(1) وحده يكفي للكشف عن الخطأ في وقت الترجمة ، ولكن (2) يجعل من السهل تجنبه عند استخدام بيان الحارس بالطريقة الواضحة.

bcmills شكرا للتوضيح

لذلك ، في res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) =? err { return Directory{}, err } يتم توسيع الماكرو =? إلى

var res *http.Reponse
{
  var err error
  res, err = ctxhttp.Get(ctx, c.HTTPClient, dirURL)
  if err != nil {
    return Directory{}, err 
  }
}

إذا كان هذا صحيحًا ، فهذا ما قصدته عندما قلت أنه يجب أن يكون له دلالات مختلفة عن := .

يبدو أنه قد يسبب التباسًا خاصًا به مثل:

func f() error {
  var err error
  g() =? err {
    if err != io.EOF {
      return err
    }
  }
  //one could expect that err could be io.EOF here but it will never be so
}

إلا إذا أسأت فهم شيء ما.

نعم ، هذا هو التوسع الصحيح. أنت محق في أنه يختلف عن := ، وهذا مقصود.

يبدو أنه سيسبب ارتباكاته الخاصة

هذا صحيح. ليس من الواضح بالنسبة لي ما إذا كان هذا سيكون مربكًا في الممارسة. إذا كان الأمر كذلك ، فيمكننا توفير ":" متغيرات من عبارة guard للإعلان (ولدينا المتغيرات "=" تُعين فقط).

(وهذا يجعلني الآن أعتقد أنه يجب تهجئة عوامل التشغيل ? و ! بدلاً من =? و =! .)

res := ctxhttp.Get(ctx, c.HTTPClient, dirURL) ?: err { return Directory{}, err }

لكن

func f() error {
  var err error
  g() ?= err { (err == io.EOF) ! { return err } }
  // err may be io.EOF here.
}

mpvl ما يشغلني بشكل رئيسي حول errd هو استخدام واجهة Handler: يبدو أنها تشجع خطوط الأنابيب ذات النمط الوظيفي لعمليات الاسترجاعات ، لكن تجربتي مع كود نمط رد الاتصال / الاستمرارية (باللغات الضرورية مثل Go و C ++ وفي الوظائف لغات مثل ML و Haskell) هو أنه غالبًا ما يكون من الصعب جدًا اتباعها من النمط المتسلسل / الحتمي المكافئ ، والذي يحدث أيضًا بالتوافق مع بقية مصطلحات Go.

هل نتصور Handler على غرار سلاسل كجزء من API، أو الخاص بك هو Handler موقفا في بعض جملة أخرى كنت تفكر (مثل شيء التشغيل على Block س؟)

bcmills ما زلت غير مشترك مع الميزات السحرية التي تقدم عشرات المفاهيم إلى اللغة في سطر واحد وتعمل فقط مع شيء واحد ، لكنني أخيرًا أدركت سبب كونها أكثر من مجرد طريقة أقصر قليلاً لكتابة x, err := f(); if err != nil { return err } . شكرًا لمساعدتي على الفهم والآسف ، فقد استغرق الأمر وقتًا طويلاً.

bcmills لقد مثالmpvl المحفز ، والذي يحتوي على بعض معالجة الأخطاء الشنيعة ، باستخدام أحدث اقتراح =? الذي لا يعلن دائمًا عن متغير خطأ جديد:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)

        defer func() {
                if r := recover(); r != nil { // r is interface{} not error so we can't use it here
                        _ = w.CloseWithError(fmt.Errorf("panic: %v", r))
                        panic(r)
                }

                if err != nil { // could use =! here but I don't see how that simplifies anything
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()

        io.Copy(w, r) =? err { return err } // what about n? does this need to be prefixed by a '_ ='?
        return nil
}

غالبية معالجة الخطأ لم تتغير. يمكنني فقط استخدام =? في مكانين. في المقام الأول ، لم تقدم حقًا أي فائدة يمكنني رؤيتها. في الثانية ، جعل الكود أطول ويخفي حقيقة أن io.Copy يُرجع شيئين ، لذلك ربما كان من الأفضل عدم استخدامه هناك.

jimmyfrasche هذا الرمز هو الاستثناء وليس القاعدة. لا ينبغي لنا تصميم ميزات لتسهيل الكتابة.

أيضًا ، أتساءل عما إذا كان يجب أن يكون recover موجودًا. إذا كان w.Write أو r.Read (أو io.Copy !) مخيفًا ، فمن الأفضل على الأرجح الإنهاء.

بدون recover ، ليست هناك حاجة حقيقية لـ defer ، ويمكن أن يصبح الجزء السفلي من الوظيفة

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

تضمين التغريدة

// r is interface{} not error so we can't use it here

لاحظ أن صياغتي المحددة (في https://github.com/golang/go/issues/21161#issuecomment-319434101) تتعلق بقيم صفرية وليست أخطاء على وجه التحديد.

// what about n? does this need to be prefixed by a '_ ='?

لم يكن الأمر كذلك ، على الرغم من أنني كان من الممكن أن أكون أكثر وضوحًا بشأن ذلك.

لا أحب بشكل خاص استخدام mpvl لـ recover في هذا المثال: إنه يشجع على استخدام الذعر على تدفق التحكم الاصطلاحي ، بينما إذا كان أي شيء أعتقد أنه يجب علينا التخلص من المكالمات الدخيلة recover ( مثل تلك الموجودة في fmt ) من المكتبة القياسية في Go 2.

مع هذا النهج ، سأكتب هذا الرمز على النحو التالي:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        io.Copy(w, r) =? err {
                w.CloseWithError(err)
                return err
        }
        return w.Close()
}

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

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
        client := storage.NewClient(ctx) =? err { return err }
        defer client.Close()

        w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
        defer func() {
                if err != nil {
                        _ = w.CloseWithError(err)
                } else {
                        err = w.Close()
                }
        }()
        defer func() {
                recover() =? r {
                        err = fmt.Errorf("panic: %v", r)
                        panic(r)
                }
        }()

        io.Copy(w, r) =? err { return err }
        return nil
}

jba the defer handler يعيد الذعر: إنه

bcmills أن المراجعة الأخيرة تبدو أفضل (حتى لو قمت بإخراج =? ، حقًا)

jba :

_ = io.Copy(w, r) =? err { _ = w.CloseWithError(err); return err }
return w.Close()

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

هذا الرمز هو الاستثناء وليس القاعدة. لا ينبغي لنا تصميم ميزات لتسهيل الكتابة.

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

jimmyfrasche : بخصوص التبسيط الخاص بك: أنت على حق تقريبًا.

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

mpvl كنت أحاول فقط

jimmyfrasche : مفهوم ، على الرغم من أنه من الجيد ألا تطلب منك واجهة برمجة التطبيقات القيام بذلك. :)

bcmills : يخدم

  • التفاف خطأ
  • تعريف لتجاهل الأخطاء (لتوضيح ذلك. انظر المثال)
  • تسجيل الأخطاء
  • مقاييس الخطأ

مرة أخرى بالترتيب من حيث الأهمية ، يجب تحديد النطاق من خلال:

  • منع
  • خط
  • حزمة

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

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

راجع للشغل ، إذا كان يجب ألا نشجع أخطاء التسجيل في المعالجات ، فمن المحتمل أن يتم إسقاط دعم السياق.

jba :

أيضًا ، أتساءل عما إذا كان التعافي يجب أن يكون هناك. إذا كان w.Write أو r.Read (أو io.Copy!) هو الذعر ، فمن الأفضل على الأرجح إنهاء.

لا تزال writeToGS تنتهي إذا كان هناك حالة من الذعر ، كما ينبغي (!!!) ، فهي تضمن فقط استدعاء CloseWithError بخطأ غير معدوم. إذا لم يتم التعامل مع حالة الذعر ، فلا يزال يتم استدعاء التأجيل ، ولكن مع الخطأ == لا شيء ، مما يؤدي إلى ملف محتمل تالف يتجسد على التخزين السحابي. الشيء الصحيح الذي يجب فعله هنا هو استدعاء CloseWithError مع بعض الأخطاء المؤقتة ثم متابعة حالة الذعر.

لقد وجدت مجموعة من الأمثلة مثل هذا في كود Go. غالبًا ما ينتج عن التعامل مع io.Pipes كود بسيط للغاية. غالبًا ما لا يكون التعامل مع الأخطاء بسيطًا كما يبدو كما رأيت الآن بنفسك.

تضمين التغريدة

لا أحب بشكل خاص استخدام mpvl للاسترداد في هذا المثال: إنه يشجع على استخدام الذعر على تدفق التحكم الاصطلاحي ،

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

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

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

في وقت الإعلان ، لا توجد حاليًا طريقة للتمييز بين "القيمة والخطأ" و "القيمة أو الخطأ":

bcmills أوه ، فهمت. لفتح علبة أخرى للدراجات ، أفترض أنه يمكنك قول ذلك

func Atoi(string) ?int

بدلا من

func Atoi(string) (int, error)

لكن ستبقى WriteString دون تغيير:

func WriteString(Writer, String) (int, error)

أنا أحب اقتراح =? / =! / :=? / :=! قبل bcmills / jba أفضل من العروض المماثلة. لها بعض الخصائص الرائعة:

  • مؤلف (يمكنك استخدام =? داخل كتلة =? )
  • عام (يهتم فقط بالقيمة الصفرية ، وليس خاصًا بنوع الخطأ)
  • تحسين النطاق
  • يمكن أن تعمل مع تأجيل (في شكل واحد أعلاه)

كما أن لديها بعض الخصائص التي لا أجدها لطيفة.

أعشاش التكوين. سيستمر الاستخدام المتكرر في وضع مسافة بادئة أبعد وأبعد جهة اليمين. هذا ليس بالضرورة أمرًا سيئًا في حد ذاته ، لكنني أتخيل أنه في المواقف التي يكون فيها التعامل مع الأخطاء المعقدة للغاية والذي يتطلب التعامل مع الأخطاء التي تسبب أخطاء تتسبب في حدوث أخطاء ، سيصبح التعامل معها بسرعة أقل وضوحًا من الوضع الراهن. في مثل هذه الحالة ، يمكن للمرء استخدام =? للخطأ الأبعد و if err != nil في الأخطاء الداخلية ، لكن هل أدى هذا إلى تحسين معالجة الأخطاء بشكل عام أو في الحالة الشائعة فقط؟ ربما يكون كل ما هو مطلوب هو تحسين الحالة الشائعة ، لكنني لا أجدها مقنعة شخصيًا.

يدخل الكذب في اللغة ليكتسب عموميتها. يتم تعريف الخطأ على أنه "(ليست) القيمة الصفرية" ، وهو أمر معقول تمامًا ، ولكن if err != nil { أفضل من if err { لأنه صريح ، في رأيي. أتوقع رؤية التواءات في البرية تحاول استخدام =? / إلخ. على مزيد من التحكم الطبيعي في التدفق لمحاولة الوصول إلى زيفه. سيكون هذا بالتأكيد أحادي الجانب ومثير للاستياء ، لكنه سيحدث. في حين أن إساءة استخدام الميزة المحتملة ليست في حد ذاتها حجة ضد الميزة ، إلا أنها شيء يجب مراعاته.

يعد تحديد النطاق المحسّن (للمتغيرات التي تعلن عن معاملتها) أمرًا جيدًا في بعض الحالات ، ولكن إذا كان النطاق بحاجة إلى الإصلاح ، فقم بإصلاح النطاق بشكل عام.

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

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

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

هل شيء مثل

func F() (S, T, error)

func MustF() (S, T) {
  return F() =? err { panic(err) }
}

مسموح؟

إذا

defer f.Close() :=? err {
    return err
}

مسموح يجب أن يكون مكافئًا (بطريقة ما) لـ

func theOuterFunc() (err error) {
  //...
  defer func() {
    if err2 := f.Close(); err2 != nil {
      err = err2
    }
  }()
  //...
}

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

على الرغم من ذلك ، بصرف النظر عن إصلاح النطاق المحسن قليلاً ، فإن هذا لا يصلح أيًا من المشكلات التي أواجهها عند التعامل مع الأخطاء في Go. في معظم حالات الكتابة ، فإن كتابة if err != nil { return err } هي مصدر إزعاج ، وتعديل مخاوف القراءة الطفيفة التي أعربت عنها في # 21182. أكبر مشكلتين هما

  1. التفكير في كيفية التعامل مع الخطأ - ولا يوجد شيء يمكن للغة أن تفعله حيال ذلك
  2. استبطان الخطأ لتحديد ما يجب القيام به في بعض المواقف - بعض الاصطلاحات الإضافية مع الدعم من الحزمة errors ستقطع شوطا طويلا هنا ، على الرغم من أنها لا تستطيع حل جميع المشاكل.

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

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

لقد كتبت كثيرًا (A LOT! آسف!) حول هذا ولكني لا أرفض الاقتراح. أعتقد حقًا أن لها ميزة ، لكنني لست مقتنعًا أنها تزيل العارضة أو تسحب ثقلها.

تضمين التغريدة

أتذكر عندما تم تقديم السلوك الحالي لـ: = - الكثير من هذا الخيط أثناء التنقل † كان صراخًا للحصول على طريقة لإضافة تعليق توضيحي صريح للأسماء التي سيتم إعادة استخدامها بدلاً من "إعادة الاستخدام الضمني فقط إذا كان هذا المتغير موجودًا في النطاق الحالي بالضبط "وهو المكان الذي تظهر فيه جميع المشكلات الدقيقة التي يصعب رؤيتها ، في تجربتي.

† لا أستطيع أن أجد هذا الموضوع هل لدى أي شخص رابط؟

أعتقد أنك لا بد أنك تتذكر موضوعًا مختلفًا ، إلا إذا كنت مشتركًا في Go عندما تم إصداره. المواصفات من 2009/11/9 ، قبل إصدارها مباشرة ، تحتوي على:

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

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

تضمين التغريدة
أعتقد أن مخادعة مثالك الأصلي هي نتيجة لـ
واجهة برمجة التطبيقات التي تستخدمها هناك بدلاً من معالجة خطأ Go نفسها.

إنه مثال مثير للاهتمام ، خاصة بسبب حقيقة ذلك
لا تريد إغلاق الملف بشكل طبيعي إذا شعرت بالذعر ، لذا فإن ملف
المصطلح العادي "تأجيل w.Close ()" لا يعمل.

إذا لم تكن بحاجة إلى تجنب استدعاء Close عندما يكون هناك ملف
هلع ، إذًا يمكنك أن تفعل:

func writeToGS(ctx context.Context, bucket, dst string, r io.Reader) (err error) {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    if err != nil {
        w.CloseWithError(err)
    }
    return err
}

بافتراض أنه تم تغيير الدلالات مثل استدعاء Close
بعد استدعاء CloseWithError هو no-op.

لا أعتقد أن هذا يبدو سيئًا للغاية بعد الآن.

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

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    defer w.Close()
    _, err = io.Copy(w, r)
    return w.Finalize(err)

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

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

rogpeppe :

لا يمكن لذلك إرفاق رسالة خطأ الذعر ، لكن التسجيل اللائق يمكن أن يجعل ذلك أكثر وضوحًا.

لا أعتقد أن هذه مشكلة.

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

r, w := io.Pipe()
go func() {
    var err error                // used to intercept downstream errors
    defer func() {
        w.CloseWithError(err)
    }()

    r, err := newReader()
    if err != nil {
        return
    }
    defer func() {
        if errC := r.Close(); errC != nil && err == nil {
            err = errC
        }
    }
    _, err = io.Copy(w, r)
}()
return r

يُظهر هذا الكود ، في حد ذاته ، أن معالجة الأخطاء يمكن أن تكون صعبة أو على الأقل فوضوية (وسأكون فضوليًا حول كيفية تحسين ذلك مع المقترحات الأخرى): إنه يمرر خلسة أخطاء المصب لأعلى من خلال متغير ولديه القليل أيضًا clunky if بيان لضمان تمرير الخطأ الصحيح. كلاهما يصرف كثيرا عن "منطق الأعمال". تهيمن معالجة الخطأ على الكود. وهذا المثال لا يتعامل مع الذعر حتى الآن.

للتأكد من اكتمالها ، في errd هذا _ سيعالج الذعر بشكل صحيح وسيبدو كما يلي:

r, w := io.Pipe()
go errd.Run(func(e *errd.E) {
    e.Defer(w.CloseWithError)

    r, err := newReader()
    e.Must(err)
    e.Defer(r.Close)

    _, err = io.Copy(w, r)
    e.Must(err)
})
return r

إذا تم تمرير القارئ أعلاه (لا يستخدم errd ) كقارئ لـ writeToGS ، والقارئ io.Reader الذي تم إرجاعه بواسطة ذعر newReader ، فسيظل ينتج عنه دلالات خاطئة مع إصلاح API المقترح (قد يتسابق إلى الإغلاق بنجاح ملف GS بعد إغلاق الأنبوب عند الذعر مع عدم وجود خطأ.)

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

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

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

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

func something() {
    r, w := io.Pipe()
    go func() {
        err := copyFromNewReader(w)
        w.CloseWithError(err)
    }()
    ...
}

func copyFromNewReader(w io.Writer) error {
    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()
    _, err = io.Copy(w, r)
    return err
}()

أفترض أن r.Close لا يُرجع خطأ مفيدًا - إذا كنت قد قرأت طوال الطريق من خلال قارئ وواجهت للتو io.EOF فقط ، فمن شبه المؤكد أنه لا يهم إذا أرجع خطأ عند إغلاقه.

لست مهتمًا بواجهة برمجة التطبيقات (API) الخاطئة - فهي حساسة جدًا لبدء تشغيل goroutines. على سبيل المثال: https://play.golang.org/p/iT441gO5us ما إذا كان doSomething يبدأ أو لا يبدأ goroutine لتشغيل
لا يجب أن تؤثر الوظيفة في على صحة البرنامج ، ولكن عند استخدام errd ، فإنها تؤثر. أنت تتوقع أن يتخطى الذعر حدود التجريد بأمان ، ولا يحدث ذلك في Go.

تضمين التغريدة

تأجيل w.CloseWithError (يخطئ)

راجع للشغل ، هذا السطر يستدعي دائمًا CloseWithError بقيمة خطأ صفري. أعتقد أنك قصدت ذلك
اكتب:

defer func() { 
   w.CloseWithError(err)
}()

تضمين التغريدة

لاحظ أن الخطأ الذي تم إرجاعه بواسطة الطريقة Close على io.Reader غير مفيد أبدًا (راجع القائمة في https://github.com/golang/go/issues/20803#issuecomment-312318808 ).

يشير ذلك إلى أننا يجب أن نكتب مثالك اليوم على النحو التالي:

r, w := io.Pipe()
go func() (err error) {
    defer func() { w.CloseWithError(err) }()

    r, err := newReader()
    if err != nil {
        return err
    }
    defer r.Close()

    _, err = io.Copy(w, r)
    return err
}()
return r

... والذي يبدو جيدًا تمامًا بالنسبة لي ، بصرف النظر عن كونه مطولًا بعض الشيء.

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

-go func() (err error) {
-   defer func() { w.CloseWithError(err) }()
+go func() (rerr error) {
+   rerr = errors.New("goroutine exited by panic")
+   defer func() { w.CloseWithError(rerr) }()

rogpeppe : في الواقع ، شكرًا. :)

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

أنت تتوقع أن يتخطى الذعر حدود التجريد بأمان ، ولا يحدث ذلك في Go.

هذا ليس ما أتوقعه. في هذه الحالة ، أتوقع ألا تبلغ واجهات برمجة التطبيقات (API) عن النجاح عندما لم يكن هناك. آخر جزء من التعليمات البرمجية الخاص بك يتعامل معه بشكل صحيح ، لأنه لا يستخدم التأجيل للكاتب. لكن هذا دقيق للغاية. قد يستخدم العديد من المستخدمين التأجيل في هذه الحالة لأنه يعتبر مصطلحًا اصطلاحيًا.

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

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

تضمين التغريدة

يدخل الكذب في اللغة ليكتسب عموميتها.

هذه نقطة جيدة جدا. تأتي المشاكل المعتادة مع الكذب من نسيان استدعاء دالة منطقية أو نسيان إحالة المؤشر إلى لا شيء.

يمكننا معالجة هذا الأخير من خلال تحديد المشغل للعمل فقط مع أنواع nillable (وربما إسقاط =! نتيجة لذلك ، لأنه سيكون في الغالب عديم الفائدة).

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

هل يُسمح بشيء مثل [ MustF

نعم.

إذا كان [ defer f.Close() :=? err { ] مسموحًا به ، فيجب أن يكون (بطريقة ما) مكافئًا لـ
[ defer func() { … }() ].

ليس بالضرورة ، لا. يمكن أن يكون لها دلالات خاصة بها (مثل call/cc أكثر من وظيفة مجهولة). لم أقترح تغييرًا في المواصفات لاستخدام =? في defer (سيتطلب ذلك تغييرًا على الأقل في القواعد النحوية) ، لذلك لست متأكدًا تمامًا من مدى تعقيد هذا التعريف .

أكبر مشكلتين هما [...] 2. استبطان الخطأ لتحديد ما يجب فعله في بعض المواقف

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

( rogpeppe ، davecheney ، dsnet ، crawshaw ، أنا ، وعدد قليل من الأشخاص الآخرين الذين نسيت بالتأكيد إجراء مناقشة لطيفة في GopherCon حول واجهات برمجة التطبيقات لفحص الأخطاء ، وآمل أن نرى بعض المقترحات الجيدة على هذا الصعيد أيضًا ، لكنني أعتقد حقًا أن هذه مسألة تتعلق بمسألة أخرى.)

bcmills : rogpeppe المذكور: خطأ تم تمريره إلى CloseWithError دائمًا لا شيء ، و 2) لا يزال لا يتعامل مع حالات الذعر ، وهذا يعني أن واجهة برمجة التطبيقات ستبلغ عن النجاح صراحة عند وجود حالة من الذعر ( قد يصدر r الذي تم إرجاعه io.EOF حتى في حالة عدم كتابة جميع وحدات البايت) ، حتى إذا تم إصلاح 1.

بخلاف ذلك ، أوافق على أنه يمكن غالبًا تجاهل الخطأ الذي تم إرجاعه بواسطة "إغلاق". ليس دائمًا ، على الرغم من ذلك (انظر المثال الأول).

أجد أنه من المدهش إلى حد ما أنه كان هناك مثل 4 أو 5 اقتراحات خاطئة تم تقديمها على أمثلتي الواضحة إلى حد ما (بما في ذلك واحدة من نفسي) وما زلت أشعر أنني يجب أن أجادل في أن معالجة الأخطاء في Go ليست تافهة. :)

bcmills :

صحيح أنه يمرر خطأ صفري إلى w.CloseWithError في حالة الذعر ، لكن البرنامج بأكمله ينتهي عند هذه النقطة على أي حال.

هل هو كذلك؟ لا يزال يتم استدعاء مؤجل ذلك الجوروتين. بقدر ما أفهم أنهم سوف يركضون حتى النهاية. في هذه الحالة ، سيشير الإغلاق إلى io.EOF.

انظر ، على سبيل المثال ، https://play.golang.org/p/5CFbsAe8zF. بعد ذعر goroutine ، لا يزال يمر بسعادة "foo" إلى goroutine الآخر الذي لا يزال يجعله يكتبه إلى Stdout.

وبالمثل ، قد تتلقى التعليمات البرمجية الأخرى io.EOF غير صحيح من goroutine المذعور (مثل ذلك الموجود في مثالك) ، وتنتهي بالنجاح ، وتلتزم بسعادة بملف إلى GS قبل أن يستأنف goroutine الذعر ذعره.

قد تكون الحجة التالية: حسنًا ، لا تكتب رمز عربات التي تجرها الدواب ، ولكن:

  • ثم يسهل عليك منع هذه الأخطاء ، و
  • قد يكون الهلع ناتجًا عن عوامل خارجية ، مثل OOMs.

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

يجب أن يتم إغلاقه مع صفر للإشارة إلى io.EOF عند الانتهاء ، لذلك لن ينجح ذلك.

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

يجب أن يتم إغلاقه مع صفر للإشارة إلى io.EOF عند الانتهاء ، لذلك لن ينجح ذلك.

ولم لا؟ سيؤدي return err في النهاية إلى تعيين rerr إلى nil .

bcmills : آه أرى ما تعنيه الآن. نعم ، يجب أن يعمل ذلك. أنا لست قلقًا بشأن عدد الأسطر ، لكنني قلق بشأن دقة الكود.

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

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

آمل ألا تمانع في اختطاف اقتراحك بصيغة بديلة - كيف يشعر الناس حيال شيء مثل هذا:

return err if f, err := os.Open("..."); err != nil

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

هذا أمر عادل ، لكن اقتراحك يجعلني غير مرتاح أيضًا - فهو يقدم بنية غير شفافة (||) تتصرف بشكل مختلف عن الطريقة التي تم تدريب المستخدمين على توقعها || يتأدب. لست متأكدًا من الحل الصحيح ، فسوف يفكر فيه أكثر.

SirCmpwn نعم ، كما قلت في

مفهوم.

هذا أكثر جذرية بعض الشيء ، ولكن ربما يكون النهج القائم على الماكرو يعمل بشكل أفضل.

f = try!(os.Open("..."))

سيأكل try! القيمة الأخيرة في المجموعة ويعيدها إذا لم يكن صفرًا ، وبخلاف ذلك يُرجع باقي المجموعة.

أود أن أقترح أن بيان مشكلتنا هو ،

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

لمعالجة بيان المشكلة هذا ، أقترح هذه الأهداف لتحسين معالجة الأخطاء في Go 2.x:

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

تقييم هذا الاقتراح:

f.Close() =? err { return fmt.Errorf(…, err) }

وفقًا لتلك الأهداف ، سأستنتج أنها نجحت جيدًا في الهدف رقم 1. لست متأكدًا من كيفية مساعدته في # 2 ، لكنه لا يجعل إضافة سياق أقل احتمالًا أيضًا (اقتراحي الخاص شارك هذا الضعف في # 2). إنه لا ينجح حقًا في # 3 و # 4 ، على الرغم من:
1) كما قال آخرون ، فإن التحقق من قيمة الخطأ والتعيين معتم وغير عادي ؛ و
2) بناء جملة =? غير معتاد أيضًا. إنه أمر محير بشكل خاص إذا تم دمجه مع بناء جملة =! مشابه ولكنه مختلف. سوف يستغرق الأمر بعض الوقت حتى يعتاد الناس على معانيهم ؛ و
3) يعد إرجاع قيمة صالحة مع الخطأ أمرًا شائعًا بدرجة كافية بحيث يجب أن يعالج أي حل جديد هذه الحالة أيضًا.

قد يكون ارتكاب الخطأ في معالجة الكتلة فكرة جيدة ، على الرغم من أنه كما اقترح الآخرون ، يتم دمجه مع التغييرات في

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

بغض النظر ، على الرغم من ذلك ، كما يشير rsc في مقالته ، نحو Go 2 ، لا من المحتمل أن تتقدم عبارة المشكلة أو الأهداف أو أي اقتراح نحوي دون تقارير الخبرة التي توضح أن المشكلة مهمة. ربما بدلاً من مناقشة المقترحات النحوية المختلفة ، يجب أن نبدأ في البحث عن البيانات الداعمة؟

بغض النظر ، على الرغم من ذلك ، كما يشير rsc في مقالته ، نحو Go 2 ، لا من المحتمل أن تتقدم عبارة المشكلة أو الأهداف أو أي اقتراح

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

if $val, err := $operation($args); err != nil {
  return err
}

عندما يكون هناك المزيد من النماذج المعيارية من الكود ، فإن المشكلة تكمن في imho البديهية.

تضمين التغريدة

أشعر أن التنسيق: f.Close() =? err { return fmt.Errorf(…, err) } مفرط في الإسهاب ومربك. أنا شخصياً لا أشعر أن جزء الخطأ يجب أن يكون في كتلة. حتمًا ، سيؤدي ذلك إلى انتشاره في 3 أسطر بدلاً من 1. علاوة على ذلك ، في التغيير الذي تحتاج إلى القيام به أكثر من مجرد تعديل خطأ قبل إعادته ، يمكن للمرء فقط استخدام if err != nil { ... } النحو.

عامل التشغيل =? هو أيضًا أمر محير قليلاً. ليس من الواضح على الفور ما يحدث هناك.

بشيء مثل هذا:
file := os.Open("/some/file") or raise(err) errors.Wrap(err, "extra context")
أو الاختزال:
file := os.Open("/some/file") or raise
والمؤجل:
defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2)
يتسم بالكلمات أكثر قليلاً ويمكن أن يؤدي اختيار الكلمة إلى تقليل الارتباك الأولي (على سبيل المثال ، قد يربط الأشخاص على الفور raise بكلمة رئيسية مشابهة من لغات أخرى مثل python ، أو يستنتجون فقط أن الزيادة تثير الخطأ / last-non- القيمة الافتراضية للمكدس للمتصل).

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

تعديل:
إذا أردنا تقليل "السحر" قليلاً ، فقد تبدو الأمثلة السابقة أيضًا كما يلي:
file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err
defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)
أنا شخصياً أعتقد أن الأمثلة السابقة أفضل ، لأنها تنقل معالجة الخطأ الكامل إلى اليمين ، بدلاً من تقسيمها كما هو الحال هنا. قد يكون هذا أوضح.

أود أن أقترح أن بيان مشكلتنا هو ، ...

أنا لا أتفق مع بيان المشكلة. أود اقتراح بديل:


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

على غرار ما يقوله Cznic ، سيكون من الجيد أن يكون لديك حل مفيد لأكثر من مجرد معالجة الأخطاء.

تتمثل إحدى طرق جعل معالجة الأخطاء أكثر عمومية في التفكير في الأمر من حيث أنواع الاتحاد / أنواع المجموع وإلغاء التغليف. كل من Swift و Rust لديهما حلول مع؟ ! بناء الجملة ، على الرغم من أنني أعتقد أن Rust كان غير مستقر بعض الشيء.

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

طعنة في بناء الجملة مستوحاة من Swift:

func Failable() (*Thingie | error) {
    ...
}

guard thingie, err := Failable() else { 
    return wrap(err, "Could not make thingie)
}
// err is not in scope here

يمكنك استخدام هذا لأشياء أخرى أيضًا ، مثل:

guard val := myMap[key] else { val = "default" }

الحل =? اقترحه bcmills و jba ليس للخطأ فقط ، المفهوم هو لغير الصفر. هذا المثال سيعمل بشكل طبيعي.

func Foo()(Bar, Recover){}
bar := Foo() =? recover { log.Println("[Info] Recovered:", recover)}

الفكرة الرئيسية لهذا الاقتراح هي الملاحظات الجانبية ، وفصل الغرض الأساسي من الكود ووضع الحالات الثانوية جانبًا ، من أجل تسهيل القراءة.
بالنسبة لي ، فإن قراءة كود Go ، في بعض الحالات ، ليست مستمرة ، وفي كثير من الأحيان توقف الفكرة بـ if err!= nil {return err} ، لذا تبدو فكرة الملاحظات الجانبية مثيرة للاهتمام بالنسبة لي ، كما هو الحال في كتاب نقرأه الفكرة الرئيسية باستمرار ثم اقرأ الملاحظات الجانبية. ( jba الحديث )
في حالات نادرة جدًا ، يكون الخطأ هو الغرض الأساسي من الوظيفة ، ربما في الاسترداد. عادةً عندما يكون لدينا خطأ ، نضيف بعض السياق ، والسجل والعودة ، وفي هذه الحالات ، يمكن للملاحظات الجانبية أن تجعل شفرتك أكثر قابلية للقراءة.
لا أعرف ما إذا كان هذا هو أفضل بناء جملة ، لا سيما أنني لا أحب الكتلة في الجزء الثاني ، يجب أن تكون الملاحظة الجانبية صغيرة ، يجب أن يكون السطر كافياً

bar := Foo() =? recover: log.Println("[Info] Recovered:", recover)

تضمين التغريدة

  1. كما قال آخرون ، فإن التحقق من قيمة الخطأ والتعيين معتم وغير عادي ؛ و

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

  1. =؟ بناء الجملة هو أيضا غير عادي. [...]

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

  1. يعد إرجاع قيمة صالحة مع الخطأ أمرًا شائعًا بدرجة كافية بحيث يجب أن يعالج أي حل جديد هذه الحالة أيضًا.

نعم هو كذلك؟

اقرأ الاقتراح بعناية: ينفذ =? المهام قبل تقييم Block ، لذلك يمكن استخدامه لهذه الحالة أيضًا:

n := r.Read(buf) =? err {
  if err == io.EOF {
    […]
  }
  return err
}

وكما لاحظ nigeltao ، يمكنك دائمًا استخدام نمط 'n ، err: = r.Read (buf) `النمط. لا تعني إضافة ميزة للمساعدة في تحديد النطاق والوضع المعياري للحالة الشائعة أنه يجب علينا استخدامها في الحالات غير الشائعة أيضًا.

ربما بدلاً من مناقشة المقترحات النحوية المختلفة ، يجب أن نبدأ في البحث عن البيانات الداعمة؟

اطلع على المشكلات العديدة (وأمثلةها) التي ربطها إيان في المنشور الأصلي.
راجع أيضًا https://github.com/golang/go/wiki/ExperienceReports#error -handling.

إذا كانت لديك رؤية محددة من تلك التقارير ، فيرجى مشاركتها.

urandom

أنا شخصياً لا أشعر أن جزء الخطأ يجب أن يكون في كتلة. سيؤدي هذا حتمًا إلى انتشاره في 3 أسطر بدلاً من 1.

الغرض من الكتلة ذو شقين:

  1. لتوفير فاصل بصري ونحوي واضح بين التعبير الذي ينتج عنه الخطأ ومعالجته ، و
  2. للسماح بمدى أوسع من معالجة الأخطاء (وفقًا المنشور الأصلي).

3 أسطر مقابل 1 لا يمثل حتى تغييرًا في اللغة: إذا كان عدد السطور هو أكبر مخاوفك ، فيمكننا معالجة ذلك بتغيير بسيط إلى gofmt .

file, err := os.Open("/some/file") or raise errors.Wrap(err, "extra context")
file, err := os.Open("/some/file") or raise err

لدينا بالفعل return و panic ؛ يبدو أن إضافة raise فوق ذلك يضيف طرقًا كثيرة جدًا للخروج من وظيفة لتحقيق مكاسب قليلة جدًا.

defer err2 := f.Close() or errors.ReplaceIfNil(err, err2)

قد يتطلب errors.ReplaceIfNil(err, err2) بعض دلالات المراجع التمريرية غير المعتادة.
يمكنك تمرير err بالمؤشر بدلاً من ذلك ، أفترض:

defer err2 := f.Close() or errors.ReplaceIfNil(&err, err2)

ولكن لا يزال يبدو غريبا جدا بالنسبة لي. هل الرمز المميز or ينشئ تعبيرًا أو بيانًا أو أي شيء آخر؟ (من شأن اقتراح أكثر تحديدًا أن يساعد).

تضمين التغريدة

ما هو البناء الملموس والدلالات لكشفك guard … else ؟ بالنسبة لي ، يبدو الأمر كثيرًا مثل =? أو :: مع تبديل الرموز والمواقع المتغيرة. (مرة أخرى ، من شأن اقتراح أكثر تحديدًا أن يساعدك: ما هي البنية الفعلية والدلالات التي تفكر فيها؟)

تضمين التغريدة
سيكون ReplaceIfNil الافتراضي بسيطًا:

func ReplaceIfNil(original, replacement error) error {
   if original == nil {
       return replacement
   }
   return original
}

لا شيء غير عادي في ذلك. ربما الاسم ...

or عامل تشغيل ثنائي ، حيث سيكون المعامل الأيسر إما IdentifierList أو PrimaryExpr. في الحالة الأولى ، يتم تقليله إلى المعرف الموجود في أقصى اليمين. ثم يسمح بتنفيذ المعامل الأيمن إذا لم يكن المعامل الأيسر قيمة افتراضية.

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

f, err := os.Open("/some/file") or return ..., errors.Wrap(err, "more context")

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

if f, err := os.Open("/some/file"); err != nil {
     return errors.Wrap(err, "more context")
}
  • إذا كانت المتغيرات المحددة أعلاه ستتاح خارج نطاق if .
    وهذا من شأنه أن يجعل من الصعب قراءة إحدى هذه العبارات فقط ، بسبب الضوضاء المرئية لكتل ​​معالجة الأخطاء هذه. وهذه واحدة من الشكاوى التي لدى الناس عند مناقشة معالجة الأخطاء في go.

urandom

or عامل تشغيل ثنائي ، حيث سيكون المعامل الأيسر إما IdentifierList أو PrimaryExpr. […] يسمح بعد ذلك بتنفيذ المعامل الأيمن إذا لم يكن المعامل الأيسر قيمة افتراضية.

عوامل التشغيل الثنائية لـ Go عبارة عن تعبيرات وليست عبارات ، لذا فإن جعل or عامل تشغيل ثنائي قد يثير الكثير من الأسئلة. (ما هي دلالات or كجزء من تعبير أكبر ، وكيف يتوافق ذلك مع الأمثلة التي نشرتها باستخدام := ؟)

بافتراض أنه عبارة في الواقع ، ما هو المعامل الأيمن؟ إذا كان تعبيرًا ، فما نوعه ، وهل يمكن استخدام raise كتعبير في سياقات أخرى؟ إذا كان بيانًا ، فما هي دلالاته إذا كان هناك أي شيء بخلاف raise ؟ أم أنك تقترح أن يكون or raise في الأساس عبارة واحدة (على سبيل المثال ، or raise كبديل لبناء الجملة لـ :: أو =?

هل يمكنني الكتابة

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

؟

هل يمكنني الكتابة

f(r.Read(buf) or raise err)

؟

defer f.Close() or raise(err2) errors.ReplaceIfNil(err, err2) or raise(err3) Transform(err3)

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

defer f.Close() or raise(err2) Transform(errors.ReplaceIfNil(err, err2)


f(r.Read(buf) or raise err)

إذا افترضنا تعليقي الأصلي - أين أو سنأخذ القيمة الأخيرة للجانب الأيسر ، بحيث إذا كانت قيمة افتراضية ، فسيتم تقييم التعبير النهائي لبقية قائمة النتائج ؛ ثم نعم ، يجب أن يكون هذا صحيحًا. في هذه الحالة ، إذا أرجع r.Read خطأ ، يتم إرجاع هذا الخطأ إلى المتصل. وإلا ، فسيتم تمرير n إلى f

تعديل:

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

ستكون هذه هي الحالة المثالية ، لكنني أيضًا بخير مع كون or raise مجرد بديل لغوي لـ =? ، طالما أنه يقبل أيضًا بيانًا بسيطًا بدلاً من الكتلة ، وذلك تغطية غالبية حالات الاستخدام بطريقة أقل تفصيلاً. أو يمكننا أيضًا استخدام قواعد تشبه الإرجاء ، حيث تقبل تعبيرًا. سيغطي هذا غالبية الحالات مثل:

f := os.Open("/some/file") or raise(err) errors.Wrap(err, "with context")

والحالات المعقدة:

f := os.Open or raise(err) func() {
     if err == io.EOF {
         […]
     }
  return err
}()

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

guard [ ASSIGNMENT || EXPRESSION ] else { [ BLOCK ] }

في حالة التعبير ، يتم تقييم التعبير وإذا كانت النتيجة لا تساوي true للتعبيرات المنطقية أو القيمة الفارغة للتعبيرات الأخرى ، يتم تنفيذ BLOCK. في مهمة ما ، يتم تقييم آخر قيمة تم تعيينها لـ != true / != nil . بعد تعليمة guard ، ستكون أي تعيينات يتم إجراؤها في النطاق (لا يُنشئ نطاق كتلة جديدًا [باستثناء ربما للمتغير الأخير؟]).

في Swift ، يجب أن يحتوي BLOCK لكشوفات الحساب guard على واحد من return أو break أو continue أو throw . لم أقرر ما إذا كنت أحب ذلك أم لا. يبدو أنه يضيف بعض القيمة لأن القارئ يعرف من الكلمة guard ما سيتبع.

هل يتابع أي شخص Swift جيدًا بما يكفي ليقول ما إذا كان هذا المجتمع يحظى باحترام guard ؟

أمثلة:

guard f, err := os.Open("/some/file") else { return errors.Wrap(err, "could not open:") }

guard data, err := ioutil.ReadAll(f) else { return errors.Wrap(err, "could not read:") }

var obj interface{}

guard err = json.Unmarshal(data, &obj) else { return errors.Wrap(err, "could not unmarshal:") }

guard m, _ := obj.(map[string]interface{}) else { return errors.New("unexpected data format") }

guard val, _ := m["key"] else { return errors.New("missing key") }

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

code, err ?= fn()

وهو ما يعني أن الوظيفة يجب أن تعود عند يخطئ! = لا شيء؟

إلى عن على: = عامل يمكننا تقديم؟: =

code, err ?:= fn()

الوضع مع؟: = يبدو أنه أسوأ بسبب التظليل ، حيث سيتعين على المحول البرمجي تمرير "err" المتغير إلى نفس قيمة إرجاع err المسماة.

أنا في الواقع متحمس جدًا لأن بعض الأشخاص يركزون على تسهيل كتابة الكود الصحيح بدلاً من مجرد تقصير الكود غير الصحيح.

بعض الملاحظات:

"تقرير تجربة" مثير للاهتمام من أحد مصممي Midori في Microsoft حول نماذج الخطأ.

أعتقد أن بعض الأفكار الواردة في هذا المستند و Swift يمكن أن تنطبق بشكل جميل على Go2.

تقديم كلمة رئيسية جديدة throws ، يمكن تعريف الوظائف مثل:

func Get() []byte throws {
  if (...) {
    raise errors.New("oops")
  }

  return []byte{...}
}

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

func ScrapeDate() time.Time throws {
  body := Get() // compilation error, unhandled throwable
  body := try Get() // we've been explicit about potential throwable

  // ...
}

في الحالات التي نعلم فيها أن الطريقة لن تفشل ، أو في الاختبارات ، يمكننا تقديم try! مشابه لـ swift.

func GetWillNotFail() time.Time {
  body := Get() // compilation error, throwable not handled
  body := try Get() // compilation error, throwable can not be propagated, because `GetWillNotFail` is not annotated with throws
  body := try! Get() // works, but will panic on throws != nil

  // ...
}

لست متأكدًا من ذلك (على غرار سويفت):

func main() {
  // 1:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err { // err contains caught throwable
    // ...
  }

  // 2:
  do {
    fmt.Printf("%v", try ScrapeDate())
  } catch err.(type) { // similar to a switch statement
    case error:
      // ...
    case io.EOF
      // ...
  }
}

ps1. قيم إرجاع متعددة func ReadRune() (ch Rune, size int) throws { ... }
ps2. يمكننا العودة بـ return try Get() أو return try! Get()
بلاي ستيشن 3. يمكننا الآن إجراء مكالمات متسلسلة مثل buffer.NewBuffer(try Get()) أو buffer.NewBuffer(try! Get())
بلاي ستيشن 4. لست متأكدًا من التعليقات التوضيحية (طريقة سهلة لكتابة errors.Wrap(err, "context") )
ps5. هذه في الواقع استثناءات
ps6. أكبر فوز هو تجميع أخطاء الوقت للاستثناءات التي تم تجاهلها

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

راجع للشغل ، نيتك في فرض الأخطاء ليتم فحصها وعدم تجاهلها يمكن أن تكون كذلك
ينطبق على أنواع غير الأخطاء أيضًا ومن الأفضل أن يكون imho في أكثر
نموذج معمم (مثل gcc __attribute __ ((warn_unused_result))).

بالنسبة لشكل المشغل ، أقترح إما نموذج قصير أو
شكل كلمة رئيسية مثل هذا:

؟ = fn () أو تحقق من fn () - نشر الخطأ للمتصل
! = fn () OR nofail fn () - هلع عند حدوث خطأ

يوم السبت ، 26 أغسطس ، 2017 الساعة 12:15 مساءً ، nvartolomei [email protected]
كتب:

بعض الملاحظات:

تقرير تجربة مثير للاهتمام
http://joeduffyblog.com/2016/02/07/the-error-model/ من أحد
مصممي Midori في Microsoft على نماذج الخطأ.

أعتقد بعض الأفكار من هذه الوثيقة و Swift
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html
يمكن تطبيقه بشكل جميل على Go2.

إدخال كلمة رئيسية جديدة رميات ، يمكن تعريف الوظائف مثل:

func الحصول على () [] رميات البايت {
إذا (...) {
رفع الأخطاء. جديد ("عفوًا")
}

إرجاع [] بايت {...}
}

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

func ScrapeDate () الوقت. رميات الوقت {
body: = Get () // خطأ في التجميع ، قابل للرمي غير معالج
body: = try Get () // لقد كنا صريحين بشأن إمكانية الرمي

// ...
}

في الحالات التي نعلم فيها أن الطريقة لن تفشل ، أو في الاختبارات ، يمكننا ذلك
أعرض حاول! على غرار سويفت.

func GetWillNotFail () time.Time {
body: = Get () // خطأ في التجميع ، يمكن رميها لم تتم معالجتها
body: = try Get () // خطأ في التجميع ، لا يمكن نشر الإصدار القابل للرمي ، لأن GetWillNotFail غير مُضاف إليه تعليقات توضيحية
الجسم: = حاول! Get () // يعمل ، لكن سيصاب بالذعر عند الرميات! = لا شيء

// ...
}

لست متأكدًا من ذلك (على غرار سويفت):

func main () {
// 1:
فعل {
fmt.Printf ("٪ v" ، جرب ScrapeDate ())
} catch Err {// err يحتوي على إمساكه قابلاً للرمي
// ...
}

// 2:
فعل {
fmt.Printf ("٪ v" ، جرب ScrapeDate ())
} catch err. (نوع) {// مشابه لتعليمة التبديل
خطأ حالة:
// ...
حالة io.EOF
// ...
}
}

ps1. قيم إرجاع متعددة رميات func ReadRune () (ch Rune، size int) {
...}
ps2. يمكننا العودة مع محاولة العودة Get () أو العودة المحاولة! احصل على()
بلاي ستيشن 3. يمكننا الآن إجراء سلسلة من المكالمات مثل buffer.NewBuffer (جرب Get ()) أو buffer.NewBuffer (جرب!
احصل على())
بلاي ستيشن 4. لست متأكدا من التعليقات التوضيحية

-
أنت تتلقى هذا لأنك علقت.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/golang/go/issues/21161#issuecomment-325106225 ، أو كتم الصوت
الخيط
https://github.com/notifications/unsubscribe-auth/AICzv9CLN77RmPceCqvjXVE_UZ6o7JGvks5sb-IYgaJpZM4Oi1c-
.

أعتقد أن عامل التشغيل الذي اقترحه jba و bcmills هو فكرة جيدة جدًا ، على الرغم من أنه أفضل

بالنظر إلى هذا المثال:

func doStuff() (int,error) {
    x, err := f() 
    if err != nil {
        return 0, wrapError("f failed", err)
    }

    y, err := g(x)
    if err != nil {
        return 0, wrapError("g failed", err)
    }

    return y, nil
}

func doStuff2() (int,error) {
    x := f()  ?? (err error) { return 0, wrapError("f failed", err) }
    y := g(x) ?? (err error) { return 0, wrapError("g failed", err) }
    return y, nil
}

أعتقد أن قراءة doStuff2 أسهل بكثير وأسرع لأنها:

  1. يهدر مساحة عمودية أقل
  2. من السهل قراءة المسار السعيد على الجانب الأيسر بسرعة
  3. من السهل قراءة شروط الخطأ على الجانب الأيمن بسرعة
  4. لا يحتوي على متغير خطأ يلوث مساحة الاسم المحلية للوظيفة

بالنسبة لي ، يبدو هذا الاقتراح وحده غير مكتمل وله الكثير من السحر. كيف سيتم تعريف عامل التشغيل ?? ؟ "يلتقط آخر قيمة عائدة إذا لم يكن لا شيء"؟ "هل تلتقط قيمة الخطأ الأخير إذا تطابق نوع الطريقة؟"

تبدو إضافة عوامل تشغيل جديدة للتعامل مع قيم الإرجاع بناءً على موضعها ونوعها بمثابة اختراق.

في 29 أغسطس 2017 ، 13:03 +0300 ، كتب Mikael Gustavsson [email protected] :

أعتقد أن عامل التشغيل الذي اقترحه jba و bcmills هو فكرة جيدة جدًا ، على الرغم من أنه أفضل
بالنظر إلى هذا المثال:
func doStuff () (int، error) {
x ، يخطئ: = f ()
إذا أخطأت! = لا شيء {
إرجاع 0 ، wrapError ("f فشل" ، يخطئ)
}

   y, err := g(x)
   if err != nil {
           return 0, wrapError("g failed", err)
   }

   return y, nil

}

func doStuff2 () (int، error) {
س: = f () ؟؟ (خطأ خطأ) {return 0، wrapError ("f فشل" ، يخطئ)}
ص: = ز (س) ؟؟ (خطأ خطأ) {return 0، wrapError ("g فشل" ، يخطئ)}
العودة ذ ، لا شيء
}
أعتقد أن قراءة doStuff2 أسهل بكثير وأسرع لأنها:

  1. يهدر مساحة عمودية أقل
  2. من السهل قراءة المسار السعيد على الجانب الأيسر بسرعة
  3. من السهل قراءة شروط الخطأ على الجانب الأيمن بسرعة
  4. لا يحتوي على متغير خطأ يلوث مساحة الاسم المحلية للوظيفة

-
أنت تتلقى هذا لأنك علقت.
قم بالرد على هذه الرسالة الإلكترونية مباشرةً ، أو اعرضها على GitHub ، أو قم بكتم صوت الموضوع.

تضمين التغريدة

كيف سيتم تعريف عامل التشغيل ?? ؟

راجع https://github.com/golang/go/issues/21161#issuecomment -319434101 و https://github.com/golang/go/issues/21161#issuecomment -320758279.

نظرًا لأن bcmills أوصى بإحياء سلسلة مثالslvmnd ،

func doStuff() (int, err) {
        x, err := f()
        return 0, wrapError("f failed", err)     if err != nil

    y, err := g(x)
        return 0, wrapError("g failed", err)     if err != nil

        return y, nil
}

ليس مقتضبًا تمامًا مثل التحقق من العبارة والخطأ في سطر واحد ، ولكنه يقرأ جيدًا بشكل معقول. (أقترح عدم السماح بـ: = شكل الإسناد في تعبير if ، وإلا فإن مشكلات النطاق ستربك الأشخاص على الأرجح حتى لو كانوا واضحين في القواعد النحوية) السماح بـ "ما لم" كنسخة مرفوضة من "if" السكر النحوي ، لكنه يعمل بشكل جيد في القراءة ويستحق التفكير فيه.

أنا لا أوصي بتناول الطعام من بيرل هنا ، على الرغم من ذلك. (Basic Plus 2 جيد) بهذه الطريقة تكمن مُعدِّلات العبارات التكرارية التي ، رغم أنها مفيدة في بعض الأحيان ، إلا أنها تجلب مجموعة أخرى من المشكلات المعقدة إلى حد ما.

نسخة أقصر:
العودة إذا أخطأت!
ينبغي أيضًا دعمها بعد ذلك.

مع هذا النحو ، فإن السؤال الذي يطرح نفسه - هل ينبغي أن تكون عبارات عدم العودة أيضا
مدعومة بعبارات "if" ، مثل هذا:
func (أرجس) إذا الشرط

ربما بدلاً من اختراع ما بعد العمل - إذا كان الأمر يستحق تقديمه بمفرده
إذا كان الخط؟

إذا أخطأت! = لا شيء يعود
إذا أخطأ! = لا شيء يُرجع 0 ، wrapError ("فشل" ، يخطئ)
إذا يخطئ! = لا شيء do_smth ()

يبدو أكثر طبيعية من أشكال خاصة من النحو ، أليس كذلك؟ على الرغم من أنني أعتقد
يسبب الكثير من الألم في التحليل: /

لكن ... كل ذلك مجرد تعديلات صغيرة وليس دعمًا خاصًا للخطأ
المناولة / التكاثر.

في يوم الإثنين 18 سبتمبر 2017 الساعة 4:14 مساءً ، كتب dsugalski [email protected] :

بما أن bcmills https://github.com/bcmills أوصى بإحياء ملف
خيط هادئ ، إذا كنا سنأخذ في الاعتبار استخدام اللغات الأخرى ،
يبدو أن مُعدِّلات العبارات ستقدم حلاً معقولاً للجميع
هذه. لأخذ مثالslvmnd https://github.com/slvmnd ، أعد بنائه باستخدام
معدِّلات البيان:

func doStuff () (int، err) {
x ، يخطئ: = f ()
إرجاع 0 ، wrapError ("f فشل" ، يخطئ) إذا يخطئ! = لا شيء

  y, err := g(x)
    return 0, wrapError("g failed", err)     if err != nil

    return y, nil

}

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

أنا لا أوصي بتناول الطعام من بيرل هنا ، على الرغم من ذلك. (Basic Plus 2 هو
بخير) بهذه الطريقة تكمن في تكرار معدّلات العبارات التي أحيانًا
مفيد ، وجلب مجموعة أخرى من القضايا المعقدة إلى حد ما.

-
أنت تتلقى هذا لأنك علقت.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/golang/go/issues/21161#issuecomment-330215402 ، أو كتم الصوت
الخيط
https://github.com/notifications/unsubscribe-auth/AICzv1rfnXeGVRRwaigCyyVK_STj-i83ks5sjmylgaJpZM4Oi1c-
.

بالتفكير أكثر في اقتراح dsugalski ، فإنه لا يحتوي على الخاصية التي طلبهاjba وآخرون ، وهي أن الرمز غير

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

ولكن بالنسبة للتمييز المرئي المستند إلى النص ، كان معيار التنسيق الذي وضعناه عندما بدأت في استخدام هذا لأول مرة منذ وقت طويل محرج هو أنه يجب أن تكون معدِّلات العبارات IF / UNLESS مبررة بشكل صحيح ، مما يجعلها تبرز بشكل جيد بما فيه الكفاية. (على الرغم من أنه تم منح معيار كان أسهل في التنفيذ وربما أكثر تميزًا بصريًا على محطة VT-220 مقارنة بالمحررين ذوي أحجام النوافذ الأكثر مرونة)

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

return 0, wrapError("f failed", err) if err != nil يمكن كتابته if err != nil { return 0, wrapError("f failed", err) }

if err != nil return 0, wrapError("f failed", err) بنفس الطريقة.

ربما كل ما هو مطلوب هنا هو أن تترك الحكومة ترك if مكتوبًا على سطر واحد على سطر واحد بدلاً من توسيعها إلى ثلاثة أسطر؟

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

على سبيل المثال ، لا يمكنني استدعاء http.Client.Do في كائن طلب جديد دون تخصيص نتيجة http.NewRequest أولاً لمتغير مؤقت ، ثم استدعاء Do على ذلك.

أتساءل عما إذا كان بإمكاننا السماح بما يلي:

f(y())

للعمل حتى إذا قام y بإرجاع (T, error) tuple. عندما تقوم y بإرجاع خطأ ، يمكن للمترجم إحباط تقييم التعبير والتسبب في إرجاع هذا الخطأ من f. إذا لم تُرجع الدالة f خطأً ، فيمكن إعطاؤها خطأً.

ثم يمكنني أن أفعل:

n, err := http.DefaultClient.Do(http.NewRequest("DELETE", "/foo", nil))

وستكون نتيجة الخطأ غير صفرية إذا فشل NewRequest أو Do.

هناك مشكلة واحدة مهمة - التعبير أعلاه صالح بالفعل إذا قبلت f وسيطتين ، أو وسيطات متغيرة. أيضًا ، من المحتمل أن تكون القواعد الدقيقة للقيام بذلك متضمنة تمامًا.

لذلك بشكل عام ، لا أعتقد أنني أحب ذلك (لست حريصًا على أي من المقترحات الأخرى في هذا الموضوع أيضًا) ، لكنني أعتقد أنني سأطرح الفكرة للنظر فيها على أي حال.

rogpeppe أو يمكنك فقط استخدام json.NewEncoder

gbbr ها نعم ، مثال سيء.

أفضل مثال قد يكون http.Request. لقد غيرت التعليق لاستخدام ذلك.

رائع. تعمل الكثير من الأفكار على جعل قابلية قراءة الكود أكثر سوءًا.
أنا بخير مع النهج

if val, err := DoMethod(); err != nil {
   // val is accessible only here
   // some code
}

شيء واحد فقط مزعج حقًا هو تحديد نطاق المتغيرات التي يتم إرجاعها.
في هذه الحالة ، يجب عليك استخدام val لكنه في نطاق if .
لذلك عليك استخدام else لكن linter سيكون ضده (وأنا أيضًا) ، والطريقة الوحيدة هي

val, err := DoMethod()
if err != nil {
   // some code
}
// some code with val

سيكون من الجيد أن يكون لديك وصول إلى المتغيرات من كتلة if :

if val, err := DoMethod(); err != nil {
   // some code
}
// some code with val

dmbreaker هذا أساسًا ما هو شرط Swift's guard. يقوم بتعيين متغير داخل النطاق الحالي إذا اجتاز بعض الشروط. انظر تعليقي السابق .

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

تضمين التغريدة
ما هو "هذا" الذي تشير إليه هنا؟ هناك عدد غير قليل من الاقتراحات المختلفة حول كيفية القيام بالأشياء.

ربما حل من جزأين؟

حدد try أنه "انزع القيمة الموجودة في أقصى اليمين في المجموعة المرتجعة ؛ إذا لم تكن القيمة الصفرية لنوعها ، فقم بإعادتها على أنها القيمة الموجودة في أقصى اليمين لهذه الوظيفة مع تعيين القيم الأخرى على الصفر". هذا يجعل الحالة المشتركة

 a := try ErrorableFunction(b)

وتمكن السلاسل

 a := try ErrorableFunction(try SomeOther(b, c))

(اختياريًا ، اجعلها غير صفرية وليست صفرية ، من أجل الكفاءة.) إذا أعادت دوال الخطأ قيمة non-nil / non-zero ، فإن الوظيفة "تحبط مع قيمة". يجب تعيين القيمة الموجودة في أقصى اليمين للدالة ed try 'للقيمة الموجودة في أقصى اليمين لوظيفة الاستدعاء أو أنها خطأ في التحقق من نوع وقت الترجمة. (لذلك ، هذا ليس مشفرًا للتعامل مع error ، على الرغم من أنه ربما يجب على المجتمع عدم تشجيع استخدامه لأي رمز "ذكي" آخر.)

بعد ذلك ، اسمح بمحاولة إرجاع الكلمات الرئيسية التي تشبه الإرجاء ، إما:

catch func(e error) {
    // whatever this function returns will be returned instead
}

أو ربما بشكل أكثر تفصيلاً ولكن بشكل أكثر انسجامًا مع كيفية عمل Go بالفعل:

defer func() {
    if err := catch(); err != nil {
        set_catch(ErrorWrapper{a, "while posting request to server"})
    }
}()

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

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

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

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

يبدو أن فصل معالجة الأخطاء إلى try و catch سيتعارض مع هذا الهدف ، على الرغم من أنني أفترض أن ذلك يعتمد على نوع البرامج التفصيلية التي تريد إضافتها عادةً.

على أقل تقدير ، أود أن أرى كيف تعمل بأمثلة أكثر واقعية.

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

لإعادة كتابة المثال الأصلي ،

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

يخرج مثل

func Chdir(dir string) error {
    catch func(e error) {
        return &PathError{"chdir", dir, e}
    }

    try syscall.Chdir(dir)
    return nil
}

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

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

كما أظن أن معظم ما يريده الناس سيُقابل

func SomethingBigger(dir string) (interface{}, error) {
     catch func (e error, filename string, lineno int) {
         return PackageSpecificError{e, filename, lineno, dir}
     }

     x := try Something()

     if x == true {
         try SomethingElse()
     } else {
         a, b = try AThirdThing()
     }

     return whatever, nil
}

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

أنا أيضا أريد حقا أن أرى

func packageSpecificHandler(f string) func (err error, filename string, lineno int) {
    return func (err error, filename string, lineno int) {
        return &PackageSpecificError{"In function " + f, err, filename, lineno}
    }
}

 func SomethingBigger(dir string) (interface{}, error) {
     catch packageSpecificHandler("SomethingBigger")

     ...
 }

أو ما يعادله ممكنًا ، لأنه عندما يعمل ذلك.

ومن بين جميع المقترحات على الصفحة ... ألا يزال هذا يبدو مثل Go؟ يبدو مثل Go أكثر من Go الحالي.

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

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

ومع ذلك ، هذا لا يعني أنه لا يوجد مجال لتحسين معالجة الأخطاء في Go.

أنا آسف لأنني لم أقرأ (حتى الآن) هذا الموضوع بالكامل (طويل جدًا) لكني أرى أشخاصًا يتخلصون من بناء جملة بديل ، لذلك أود مشاركة فكرتي:

a, err := helloWorld(); err? {
  return fmt.Errorf("helloWorld failed with %s", err)
}

آمل ألا أفوت شيئًا أعلاه يبطل هذا. أعدك بأنني سأحصل على جميع التعليقات يومًا ما :)

يجب أن يُسمح للعامل فقط على النوع error ، على ما أعتقد ، لتجنب الفوضى الدلالية لتحويل النوع.

مثير للاهتمام ،

if a, err := helloWorld(); err != nil {
  return fmt.Errorf("helloWorld failed with %s", err)
}

أرى أنه سيسمح لـ a بالهروب ، بينما في الحالة الحالية ، يتم تحديد نطاقه إلى كتل then and else.

@ object88 أنت على حق ، التغيير دقيق وجمالي وذاتي. أنا شخصياً ، كل ما أريده من Go 2 في هذا الموضوع هو تغيير دقيق في قابلية القراءة.

أنا شخصياً أجده أكثر قابلية للقراءة لأن السطر لا يبدأ بـ if ولا يتطلب !=nil . المتغيرات موجودة على الحافة اليسرى حيث توجد (معظم؟) خطوط أخرى.

نقطة رائعة في نطاق a ، لم أفكر في ذلك.

بالنظر إلى الاحتمالات الأخرى لهذه القواعد ، يبدو أن هذا ممكن.

err := helloWorld(); err? {
  return fmt.Errorf("error: %s", err)
}

وربما

helloWorld()? {
  return fmt.Errorf("hello world failed")
}

التي ربما تتفكك فيها.

ربما يجب أن تكون إعادة الخطأ جزءًا من كل استدعاء دالة في Go ، لذلك يمكنك أن تتخيل:
""
أ: = helloWorld () ؛ يخطئ؟ {
إرجاع fmt.Errorf ("فشل helloWorld:٪ s" ، يخطئ)
}

ماذا عن التعامل مع استثناءات حقيقية؟ أعني جرب ، التقط ، أخيرًا بدلاً من ذلك مثل العديد من اللغات الحديثة؟

كلا ، فهو يجعل الكود ضمنيًا وغير واضح (على الرغم من أنه أقصر قليلاً)

يوم الخميس ، 23 تشرين الثاني (نوفمبر) 2017 الساعة 07:27 ، Kamyar Miremadi [email protected]
كتب:

ماذا عن التعامل مع استثناءات حقيقية؟ أعني جرب ، قبض ، أخيرًا
بدلا من ذلك مثل العديد من اللغات الحديثة؟

-
أنت تتلقى هذا لأنك علقت.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/golang/go/issues/21161#issuecomment-346529787 ، أو كتم الصوت
الخيط
https://github.com/notifications/unsubscribe-auth/AICzvyy_kGAlcs6RmL8AKKS5deNRU4_5ks5s5PQVgaJpZM4Oi1c-
.

العودة إلىmpvl الصورة WriteToGCS سبيل المثال يصل موضوع ، أود أن أقترح (مرة أخرى) أن الالتزام / النمط العودة إلى الحالة السابقة لا يكفي المشتركة لتبرير تغيير كبير في معالجة الأخطاء الذهاب ل. ليس من الصعب التقاط النمط في دالة ( رابط الملعب ):

func runWithCommit(f, commit func() error, rollback func(error)) (err error) {
    defer func() {
        if r := recover(); r != nil {
            rollback(fmt.Errorf("panic: %v", r))
            panic(r)
        }
    }()
    if err := f(); err != nil {
        rollback(err)
        return err
    }
    return commit()
}

ثم يمكننا كتابة المثال كـ

func writeToGCS(ctx context.Context, bucket, dst string, r io.Reader) error {
    client, err := storage.NewClient(ctx)
    if err != nil {
        return err
    }
    defer client.Close()

    w := client.Bucket(bucket).Object(dst).NewWriter(ctx)
    return runWithCommit(
        func() error { _, err := io.Copy(w, r); return err },
        func() error { return w.Close() },
        func(err error) { _ = w.CloseWithError(err) })
}

أود أن أقترح حلًا أبسط:

func someFunc() error {
    ^err := someAction()
    ....
}

لإرجاع دوال متعددة:

func someFunc() error {
    result, ^err := someAction()
    ....
}

ولحجج الإرجاع المتعددة:

func someFunc() (result Result, err error) {
    var result Result
    params, ^err := someAction()
    ....
}

^ تعني الإشارة العودة إذا لم تكن المعلمة صفرية.
بشكل أساسي "نقل الخطأ لأعلى في المكدس إذا حدث"

أي عيوب في هذه الطريقة؟

تضمين التغريدة
كيف يمكن تعديل الخطأ قبل إعادته؟

urandom
التفاف الأخطاء هو إجراء مهم في رأيي يجب القيام به صراحة.
كود Go يتعلق بقابلية القراءة وليس السحر.
أود إبقاء التفاف الخطأ أكثر وضوحًا

لكن في نفس الوقت أود التخلص من الكود الذي لا يحمل الكثير من المعلومات ويأخذ المساحة فقط.

if err != nil {
    return err
}

إنها مثل مبتذلة Go - لا تريد قراءتها ، تريد تخطيها فقط.

ما رأيته حتى الآن في هذه المناقشة هو مزيج من:

  1. تقليل الإسهاب النحوي
  2. تحسين الخطأ عن طريق إضافة سياق

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

1. تقليل الإسهاب النحوي

تعجبني فكرة gladkikhartem ، حتى في شكلها الأصلي الذي

 result, ^ := someAction()

في سياق func:

func getOddResult() (int, error) {
    result, ^ := someResult()
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

هذه الصيغة القصيرة - أو بالشكل الذي اقترحه gladkikhartem بـ err^ - ستعالج جزء الإسهاب من المشكلة (1).

2. سياق الخطأ

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

عند استخدام contextError ، بطريقة ما يجب على Go إرفاق تتبع مكدس الاستدعاءات عند النقطة التي تم فيها إنشاء الخطأ بالضبط.

مرة أخرى مع مثال func:

func getOddResult() (int, contextError) {
    result, ^ := someResult() // here a 'contextError' is created; if the error received from 'someResult()' is also a `contextError`, the two are nested
    if result % 2 == 0 {
          return result + 1, nil
    }
    return result, nil
}

تم تغيير النوع فقط من error إلى contextError ، والذي يمكن تعريفه على النحو التالي:

type contextError interface {
    error
    Stack() []StackEntry
    Cause() contextError
}

(لاحظ كيف يختلف هذا Stack() عن https://golang.org/pkg/runtime/debug/#Stack ، لأننا نأمل في الحصول على نسخة غير بايت من مكدس الاستدعاءات goroutine هنا)

ستؤدي الطريقة Cause() إلى عدم إرجاع أو إرجاع contextError نتيجة التداخل.

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

فن مسبق:

  • https://godoc.org/github.com/pkg/errors (لا أعرف هذه الحزمة على نطاق واسع أو أقترح أنه يجب تصميمها حولها ، لكنها واحدة من أكثرها استخدامًا / معروفًا)

مجرد غذاء للفكر.

تضمين التغريدة

أعتقد أنك قد فاتك الهدف من هذا الاقتراح. المنشور الأصلي لكل من إيان:

من السهل بالفعل (ربما من السهل جدًا) تجاهل الخطأ (انظر # 20803). تعمل العديد من المقترحات الحالية الخاصة بمعالجة الأخطاء على تسهيل إعادة الخطأ بدون تعديل (على سبيل المثال ، # 16225 ، # 18721 ، # 21146 ، # 21155). القليل يسهل إرجاع الخطأ بمعلومات إضافية.

غالبًا ما يكون إرجاع الأخطاء غير المعدلة خطأً ، وعادةً على الأقل غير مفيد. نريد أن نشجع المعالجة الدقيقة للأخطاء: معالجة حالة الاستخدام "العودة غير المعدلة" فقط من شأنها أن تؤدي إلى تحيز الحوافز في الاتجاه الخاطئ.

bcmills إذا تمت إضافة السياق (في شكل تتبع مكدس) ، فسيتم إرجاع الخطأ بمعلومات إضافية. هل يمكن اعتبار إرفاق رسالة يمكن للبشر قراءتها ، مثل "خطأ أثناء إدراج السجل" ، بمثابة "معالجة دقيقة للأخطاء"؟ كيف تقرر عند أي نقطة في مكدس الاستدعاءات يجب إضافة مثل هذه الرسائل (في كل وظيفة ، أعلى / أسفل ، إلخ)؟ هذه كلها أسئلة شائعة أثناء معالجة أخطاء الترميز.

يمكن مواجهة "الإرجاع غير المعدل" كما هو موضح أعلاه مع "الإرجاع غير المعدل باستخدام تتبع التراص" افتراضيًا ، و (بأسلوب تفاعلي) إضافة رسالة يمكن قراءتها بواسطة الإنسان حسب الحاجة. لم أحدد كيف يمكن إضافة هذه الرسالة التي يمكن للبشر قراءتها ، ولكن يمكن للمرء أن يرى كيف يعمل الالتفاف في pkg/errors لبعض الأفكار.

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

تضمين التغريدة
أوافق بنسبة 100٪ مع # 20803 على أنه يجب دائمًا معالجة الأخطاء أو تجاهلها صراحةً (وليس لدي أي فكرة عن سبب عدم القيام بذلك من قبل ...)
نعم ، لم أتطرق إلى نقطة الاقتراح ولست مضطرًا إلى ذلك. أهتم بالحل الفعلي المقترح ، وليس النوايا من ورائه ، لأن النوايا لا تتطابق مع النتائج. وعندما أرى || مثل || الأشياء التي يتم اقتراحها - إنها تجعلني حزينًا حقًا.

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

أحب أن يذكرني Go بالتعامل مع الأخطاء ، لكني أكره عندما يشجعني التصميم على شيء مشكوك فيه.

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

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

أطلب من كليهما تعزيز فهمي لأفضل ممارسات Go (كلما كانت متعارف عليها إلى حد ما) ولأنني أشعر أن مثل هذا التعريف قد يكون مفتاحًا لتقديم بعض المقترحات نحو تحسين الوضع الحالي.

gladkikhartem أعلم أن هذا الاقتراح if err != nil { return err } ، وتلك هي المكان المناسب لمناقشة البنية التي تعمل على تحسين هذه الحالة المحددة فقط. شكرا.

تضمين التغريدة
آسف إذا نقلت المناقشة خارج الطريق.

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

type MyError struct {
    Type int
    Message string
    Context string
    Err error
}

func VeryLongFunc() error {
    var err MyError
    err.Context = "general function context"


   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
   }

    // in case we need to make a cleanup after error

   result, ^err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       file.Close()
   }

   // another variant with different symbol and return statement

   result, ?err.Err := someAction() {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }

   // using original approach

   result, err.Err := someAction()
   if err != nil {
       err.Type = PermissionError
       err.Message = fmt.SPrintf("some action has no right to access file %v: ", file)
       return err
   }
}

func main() {
    err := VeryLongFunc()
    if err != nil {
        e := err.(MyError)
        log.Print(e.Error(), " in ", e.Dir)
    }
}

يستخدم الرمز ^ للإشارة إلى معلمة الخطأ ، بالإضافة إلى التمييز بين تعريف الوظيفة ومعالجة الخطأ لـ "someAction () {}"
يمكن حذف {} إذا تم إرجاع الخطأ بدون تعديل

إضافة بعض الموارد الإضافية للرد على دعوتي الخاصة لتحديد "المعالجة الدقيقة للأخطاء" بشكل أفضل:

على الرغم من أن النهج الحالي ممل ، أعتقد أنه أقل إرباكًا من البدائل ، على الرغم من وجود سطر واحد إذا كانت العبارات قد تنجح؟ يمكن؟

blah, err := doSomething()
if err != nil: return err

...او حتى...

blah, err := doSomething()
if err != nil: return &BlahError{"Something",err}

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

لقد كنت من محبي البرمجة الموجهة للسكك الحديدية ، وتأتي الفكرة من بيان Elixir with .
else block بمجرد قصر e == nil .

هذا هو اقتراحي مع رمز زائف في المستقبل:

func Chdir(dir string) (e error) {
    with e == nil {
            e = syscall.Chdir(dir)
            e, val := foo()
            val = val + 1
            // something else
       } else {
           printf("e is not nil")
           return
       }
       return nil
}

ardhitama أليس هذا إذن مثل "جرب إمساك" باستثناء "مع" مثل عبارة "جرب" وأن "آخر" مثل "كاتش"؟
لماذا لا ننفذ معالجة الاستثناءات مثل Java أو C #؟
في الوقت الحالي ، إذا كان المبرمج لا يريد معالجة الاستثناء في هذه الوظيفة ، فإنه يعيدها كنتيجة لتلك الوظيفة. لا تزال هناك طريقة لإجبار المبرمجين على التعامل مع استثناء إذا لم يرغبوا في ذلك وفي كثير من الأحيان لا تحتاج إلى ذلك حقًا ، ولكن ما نحصل عليه هنا هو الكثير من إذا كان يخطئ! وغير مقروء (الكثير من الضوضاء). أليس هذا هو السبب وراء اختراع عبارة Try Catch Last في المقام الأول في لغة برمجة أخرى؟

لذا ، أعتقد أنه من الأفضل Go Authors "حاول" ألا تكون عنيدًا !! وقم فقط بتقديم عبارة "Try Catch finally" في الإصدارات التالية. شكرا لك.

تضمين التغريدة
لا يمكنك إدخال معالجة الاستثناءات في go ، لأنه لا توجد استثناءات في Go.
إن تقديم try {} catch {} في Go يشبه تقديم try {} catch {} في لغة C - إنه خطأ تمامًا .

تضمين التغريدة
ماذا عن عدم تغيير معالجة أخطاء Go على الإطلاق ، بل تغيير أداة gofmt مثل هذه لمعالجة الخطأ أحادي الخط؟

err := syscall.Chdir(dir)
    if err != nil {return &PathError{"chdir", dir, err}}
err = syscall.Chdir(dir2)
    if err != nil {return err}

إنه متوافق مع الإصدارات السابقة ويمكنك تطبيقه على مشاريعك الحالية

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

KamyarM من حيث الجوهر ، لكنها ليست كذلك من الناحية العملية. في رأيي لأننا صريحون هنا ولا نكسر أي مصطلحات Go.

لماذا ا؟

  1. لا يمكن للتعبيرات الموجودة داخل عبارة with التصريح عن var جديد ، ومن ثم فهي تنص صراحة على أن الهدف هو التقييم خارج المتغيرات الكتلية.
  2. ستعمل كشوفات الحساب داخل with كما لو كانت داخل كتلة try و catch . في الواقع ، سيكون أبطأ لأنه في كل تعليمات تالية تحتاج إلى تقييم ظروف with في سيناريو أسوأ الحالات.
  3. حسب التصميم ، فإن النية هي إزالة if s الزائدة وليس إنشاء معالج استثناء لأن المعالج سيكون دائمًا محليًا (تعبير with و else block).
  4. لا حاجة لفك المكدس بسبب throw

ملاحظة. أرجوا أن تصحح لي إذا كنت مخطئا.

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

تضمين التغريدة
نعم ، لذلك أقترح اعتماد "البرمجة الموجهة للسكك الحديدية" بدلاً من ذلك ومحاولة عدم إزالة الصريح. إنها مجرد زاوية أخرى لمهاجمة المشكلة ، فالحلول الأخرى تريد حلها من خلال عدم السماح للمترجم بكتابة if err != nil تلقائيًا نيابة عنك.

with أيضًا ليس فقط لمعالجة الأخطاء ، بل يمكن أن يكون مفيدًا لأي تدفق تحكم آخر.

تضمين التغريدة
اسمحوا لي أن أوضح أنني أعتقد أن الكتلة Try Catch Finally جميلة. If err!=nil ... هو في الواقع الرمز القبيح.

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

تضمين التغريدة

أنا بخير إذا أطلق عليها Go Authors اسم Go ++ أو Go # أو GoJava وتقديم Try Catch Finally هناك ؛)

تضمين التغريدة

يعد تجنب التغيير غير الضروري أمرًا ضروريًا - وهو أمر بالغ الأهمية - لأي مسعى هندسي. عندما يقول الناس تغييرًا في هذا السياق ، فإنهم يقصدون حقًا "التغيير للأفضل" ، وهو ما ينقلونه بفاعلية من خلال _ الحجج_ التي توجه الفرضية إلى هذا الاستنتاج المقصود.

_ فقط افتح عقلك يا رجل! _ النداء غير مقنع. ومن المفارقات ، أنه يحاول أن يقول شيئًا يعتبره معظم المبرمجين قديمًا ورائعًا - جديدًا ومُحسَّنًا -.

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

اكتشفت أن الكثيرين في Go Community ينظرون إلى الأمر على أنه دينهم وليسوا منفتحين على التغيير أو الاعتراف بالأخطاء.

يعتمد Go على البحث الأكاديمي ؛ الآراء الشخصية لا تهم.

حتى أن المطورين الرئيسيين لمترجم C # من Microsoft أقروا علنًا بأن _exceptions_ هي طريقة سيئة لإدارة الأخطاء ، مع منح الأفضلية لنموذج Go / Rust كبديل أفضل: http://joeduffyblog.com/2016/02/07/the-error-model/

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

@ دكتور- رهيب شكرا لك على المقال.

لكني لم أجد أي مكان يذكر GoLang كلغة أكاديمية.

راجع للشغل ، لتوضيح وجهة نظري ، في هذا المثال

func Execute() error {
    err := Operation1()
    if err!=nil{
        return err
    }

    err = Operation2()
    if err!=nil{
        return err
    }

    err = Operation3()
    if err!=nil{
        return err
    }

    err = Operation4()
    return err
}

يشبه تنفيذ معالجة الاستثناءات في C # مثل هذا:

         public void Execute()
        {

            try
            {
                Operation1();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation2();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation3();
            }
            catch (Exception)
            {
                throw;
            }
            try
            {
                Operation4();
            }
            catch (Exception)
            {
                throw;
            }
        }

أليست هذه طريقة رهيبة للتعامل مع الاستثناءات في C #؟ جوابي هو نعم ، لا أعلم عن إجابتك! في Go ليس لدي خيار آخر. إنه ذلك الاختيار الرهيب أو الطريق السريع. هذا هو الحال في GO وليس لدي خيار.

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

راجع للشغل ، أعلم أن GO يحتوي على Panic, Recover , Defer غير موصى به وهو نوع مشابه لـ Try Catch Finally لكن في رأيي الشخصي ، بناء الجملة Try Catch Finally هو طريقة أكثر نظافة وأفضل تنظيماً للتعامل مع الاستثناءات.

@ دكتور- رهيب

يرجى أيضًا التحقق من ذلك:
https://github.com/manucorporat/try

KamyarM ، لم يقل أن Go هي لغة أكاديمية ، بل قال إنها تستند إلى بحث أكاديمي. لم تكن المقالة عن Go ، لكنها تحقق في نموذج معالجة الخطأ الذي استخدمته Go.

إذا وجدت أن manucorporat/try يناسبك ، فالرجاء استخدامه في الكود الخاص بك. لكن التكاليف (الأداء ، وتعقيد اللغة ، وما إلى ذلك) لإضافة try/catch إلى اللغة نفسها لا تستحق المقايضة.

تضمين التغريدة
أنت مثال غير دقيق. بديل ل

    err := Operation1()
    if err!=nil {
        return err
    }
    err = Operation2()
    if err!=nil{
        return err
    }
    err = Operation3()
    if err!=nil{
        return err
    }
    return Operation4()

سوف يكون

            Operation1();
            Operation2();
            Operation3();
            Operation4();

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

         err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

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

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

ومع ذلك ، لا أريد خطأ invalid HTTP header عندما يحتوي طلبي على JSON request body مشوهًا ، معالجة الاستثناءات هي زر إطلاق النار والنسيان السحري الذي يحقق ذلك في واجهات برمجة تطبيقات C ++ و C # التي تستخدم معهم.

بالنسبة لتغطية كبيرة لواجهة برمجة التطبيقات ، من المستحيل توفير سياق خطأ كافٍ لتحقيق معالجة ذات مغزى للأخطاء. ذلك لأن أي تطبيق جيد يتعامل مع الأخطاء بنسبة 50٪ في Go ، ويجب أن يكون 90٪ بلغة تتطلب نقل تحكم غير محلي للتعامل مع الأخطاء.

تضمين التغريدة

الطريقة البديلة التي ذكرتها هي الطريقة الصحيحة لكتابة الكود في C #. إنه عبارة عن 4 أسطر فقط من الكود ويظهر مسار التنفيذ السعيد. ليس لديها تلك الضوضاء if err!=nil . في حالة حدوث استثناء ، يمكن للوظيفة التي تهتم بهذه الاستثناءات التعامل معها باستخدام Try Catch Finally (يمكن أن تكون الوظيفة نفسها أو المتصل أو المتصل بالمتصل أو المتصل بمتصل المتصل ... أو مجرد معالج حدث يعالج جميع الأخطاء غير المعالجة في تطبيق ما. للمبرمج خيارات مختلفة.)

err := Operation1()
    if err!=nil {
        log.Print("input error", err)
                fmt.Fprintf(w,"invalid input")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation2()
    if err!=nil{
        log.Print("controller error", err)
                fmt.Fprintf(w,"operation has no meaning in this context")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    err = Operation3()
    if err!=nil{
        log.Print("database error", err)
                fmt.Fprintf(w,"unable to access database, try again later")
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }

تبدو بسيطة ولكنها صعبة. أعتقد أنه يمكنك تجميع نوع خطأ مخصص يحمل خطأ النظام وخطأ المستخدم (بدون تسريب الحالة الداخلية للمستخدم الذي قد لا يكون لديه أفضل النوايا) ورمز HTTP.

func Chdir(dir string) error {
    if e := syscall.Chdir(dir); e != nil {
        return &PathError{"chdir", dir, e}
    }
    return nil
}

لكن حاول لمرة

func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err}:nil;
}
func Chdir(dir string) error {
    return  syscall.Chdir(dir) ? &PathError{"chdir", dir, err};
}



md5-9bcd2745464e8d9597cba6d80c3dcf40



```go
func Chdir(dir string) error {
    n , _ := syscall.Chdir(dir):
               // something to do
               fmt.Println(n)
}

كل هؤلاء يحتويون على نوع من السحر غير الواضح ، والذي لا يبسط الأمور للقارئ. في المثالين السابقين ، يصبح err نوعًا من الكلمات الرئيسية الزائفة أو متغيرًا يحدث تلقائيًا. في المثالين الأخيرين ، ليس من الواضح على الإطلاق ما الذي يفترض أن يقوم به عامل التشغيل : - هل سيعود الخطأ تلقائيًا؟ هل RHS للمشغل عبارة واحدة أم كتلة؟

FWIW ، سأكتب دالة مجمعة حتى تتمكن من تنفيذ return newPathErr("chdir", dir, syscall.Chdir(dir)) وستقوم تلقائيًا بإرجاع خطأ nil إذا كانت المعلمة الثالثة لا شيء. :-)

IMO ، أفضل اقتراح رأيته لتحقيق أهداف "تبسيط معالجة الأخطاء في Go" و "إرجاع الخطأ بمعلومات سياقية إضافية" هو من mrkaspa في # 21732:

a, b, err? := f1()

يتوسع إلى هذا:

if err != nil {
   return nil, errors.Wrap(err, "failed")
}

ويمكنني أن أجبره على الذعر من هذا:

a, b, err! := f1()

يتوسع إلى هذا:

if err != nil {
   panic(errors.Wrap(err, "failed"))
}

سيؤدي هذا إلى الحفاظ على التوافق مع الإصدارات السابقة ، وسيعمل على إصلاح جميع نقاط الألم في معالجة الأخطاء أثناء التنقل

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

ال ؟ سيقلل معدل الخطأ المعياري تسليم الوظائف و! سيفعل التعديل الشيء نفسه في الأماكن التي سيتم استخدام التأكيد فيها بلغات أخرى مثل بعض الوظائف الرئيسية.

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

في حال كان لديك ...

func foo() (int, int, error) {
    a, b, err? := f1()
    return a, b, nil
}
func bar() (int, error) {
    a, b, err? := foo()
    return a+b, nil
}

إذا حدث خطأ ما في foo ، ثم في موقع الاتصال bar foo ، فسيتم تغليف الخطأ بنفس النص بشكل مضاعف ، دون إضافة أي معنى. على الأقل ، سأعارض الجزء errors.Wrap من الاقتراح.

لكن التوسع أكثر ، ما هي النتيجة المتوقعة من ذلك؟

func baz() (a, b int, err error) {
  a = 1
  b = 2
  a, b, err? = f1()
  return

هل أعيد تخصيص a و b لقيم صفرية؟ إذا كان الأمر كذلك ، فهذا سحر ، أشعر أنه يجب علينا تجنبه. هل ينفذون القيم التي تم تعيينها مسبقًا؟ (أنا لا أهتم بقيم الإرجاع المسماة بنفسي ، لكن لا يزال يتعين أخذها في الاعتبار لغرض هذا الاقتراح).

@ dup2X دعنا نزيل اللغة ، يجب أن تكون بهذه الطريقة

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

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

مع احترام ، لا أوافق ،creker. لدينا أمثلة على هذا السيناريو في Go stdlib لقيم الإرجاع non-nil حتى في حالة وجود خطأ غير صفري ، وهي في الواقع وظيفية ، مثل العديد من الوظائف في bufio.Reader Struct . نحن مبرمجي Go نشجعهم على فحص / معالجة جميع الأخطاء ؛ إن تجاهل الأخطاء يبدو أكثر فظاعة من الحصول على قيم إرجاع غير صفرية وخطأ. في حالة الاستشهاد ، إذا قمت بإرجاع لا شيء ولم تتحقق من الخطأ ، فقد تستمر في العمل على قيمة غير صالحة.

لكن إذا وضعنا ذلك جانبًا ، فلنتفحص هذا الأمر أكثر قليلاً. ما هي دلالات عامل التشغيل ? ؟ هل يمكن تطبيقه فقط على الأنواع التي تطبق الواجهة error ؟ هل يمكن تطبيقه على أنواع أخرى أو إرجاع وسيطات؟ إذا كان من الممكن تطبيقه على الأنواع التي لا تنفذ الخطأ ، فهل يتم تشغيله بواسطة أي قيمة / مؤشر غير صفري؟ هل يمكن تطبيق عامل التشغيل ? على أكثر من قيمة إرجاع ، أم أن هذا خطأ في المترجم؟

تضمين التغريدة
إذا كنت تريد فقط إرجاع الخطأ دون إرفاق أي شيء مفيد به - فسيكون من الأسهل بكثير إخبار المترجم بالتعامل مع جميع الأخطاء غير المعالجة مثل "إذا كان خطأ! = لا شيء يعود ..." ، على سبيل المثال:

func doStuff() error {
    doAnotherStuff() // returns error
}

func doStuff() error {
    res := doAnotherStuff() // returns string, error
}

وليس هناك حاجة لمزيد من الجنون؟ رمز في هذه الحالة.

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

@ object88

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

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

gladkikhartem ، المشكلة في if err! = لا شيء هو أن المنطق الفعلي ضاع تمامًا فيه وعليك البحث عنه بنشاط إذا كنت تريد فهم ما يفعله الكود في مساره الناجح ولا يهتم بكل هذا التعامل مع الأخطاء . يصبح الأمر أشبه بقراءة الكثير من كود C حيث يكون لديك عدة أسطر من الكود الفعلي وكل شيء آخر هو مجرد فحص للأخطاء. يلجأ الأشخاص حتى إلى وحدات الماكرو التي تلتف بكل ذلك وتنتقل إلى نهاية الوظيفة.

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

يعمل هذا في الملعب ، إذا لم تقم بإعادة تنسيقه:

a, b, err := Frob("one string"); if err != nil { return a, b, fmt.Errorf("couldn't frob: %v", err) }
// continue doing stuff with a and b

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

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

لا أعتقد أن الجزء الذي يسبق الفاصلة المنقوطة (المهمة) يجب أن يتغير ، حتى نترك 69 حرفًا قد نقطعها. من بين هؤلاء ، 49 هي عبارة الإرجاع ، والقيم المراد إرجاعها ، وتغليف الخطأ ، ولا أرى قيمة كبيرة في تغيير بناء الجملة (على سبيل المثال ، من خلال جعل عبارات الإرجاع اختيارية ، مما يربك المستخدمين).

بحيث يترك هذا البحث عن اختصار لـ ; if err != nil { _ } حيث تمثل الشرطة السفلية جزءًا من الكود. أعتقد أن أي اختصار يجب أن يتضمن صراحةً err للوضوح حتى لو كان ذلك يجعل المقارنة الصفرية غير مرئية إلى حد ما ، لذلك يتبقى لنا الخروج باختصار مقابل ; if _ != nil { _ } .

تخيل للحظة أننا نستخدم شخصية واحدة. سأختار § كعنصر نائب مهما كانت هذه الشخصية. سيكون سطر الكود بعد ذلك:

a, b, err := Frob("one string") § err return a, b, fmt.Errorf("couldn't frob: %v", err)

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

هذا هو 88 حرفًا ، مما يوفر إجماليًا إجماليًا يبلغ 12 حرفًا في سطر بطول 100 حرف.

لذا فإن سؤالي هو: هل يستحق الأمر فعلاً؟

تحرير: أعتقد أن وجهة نظري هي أنه عندما ينظر الناس إلى كتل Go's if err != nil ويقولون "أتمنى أن نتخلص من هذا الهراء" ، فإن 80-90٪ مما يتحدثون عنه هو أشياء تمتلكها بطبيعتها لتفعله من أجل معالجة الأخطاء_. الحمل الفعلي الناتج عن بناء جملة Go ضئيل.

lpar ، أنت في الغالب تتبع نفس المنطق الذي طبقته أعلاه ، لذلك بطبيعة الحال أتفق مع منطقك. لكنني أعتقد أنك تستبعد الجاذبية المرئية لوضع جميع عناصر الخطأ في الاتجاه الصحيح:

a, b := Frob("one string")  § err { return ... }

أكثر قابلية للقراءة بعامل يتجاوز مجرد تقليل الأحرف.

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

a, b, err? := Frob("one string")

كانت مشكلة أخطاء Go بالنسبة لي دائمًا الافتقار إلى السياق. لا تعد سلاسل الإرجاع والتفاف مفيدة على الإطلاق لتحديد مكان حدوث الخطأ بالفعل. هذا هو السبب في أن github.com/pkg/errors على سبيل المثال ، أصبح أمرًا ضروريًا بالنسبة لي. مع وجود أخطاء من هذا القبيل ، أحصل على مزايا معالجة الخطأ Go للبساطة وفوائد الاستثناءات التي تلتقط السياق تمامًا وتتيح لك العثور على مكان الفشل الدقيق.

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

أعتقد أن السؤال عما إذا كان fmt.Errorf "عديم الفائدة" مقارنة بـ errors.Wrap متعامد مع هذه المشكلة ، حيث إن كلاهما مطول بشكل متساوٍ. (في التطبيقات الفعلية التي لا أستخدمها أيضًا ، أستخدم شيئًا آخر يسجل الخطأ وسطر الكود الذي حدث فيه.)

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

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

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

تضمين التغريدة

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

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

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

@كما

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

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

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

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

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

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

creker حاولت إيضاح نفس النقطة وطلبت توضيح ما يمكن اعتباره رسالة خطأ مفيدة / مفيدة.

الحقيقة هي أنني سأقدم في أي يوم رسالة خطأ متغيرة الجودة (والتي لديها تحيز المطور في كتابتها في تلك اللحظة وبتلك المعرفة) في مقابل مكدس الاستدعاءات وحجج الوظيفة. بنص رسالة خطأ ( fmt.Errorf أو errors.New ) ينتهي بك الأمر بالبحث في النص في الكود المصدري ، بينما تقرأ مكدسات المكالمات / backtraces (التي تبدو مكروهة وآمل ألا تكون لأسباب جمالية) يتوافق مع البحث مباشرة عن طريق رقم الملف / السطر ( errors.Wrap وما شابه).

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

حول هذا الموضوع ، ربما تكون المشكلة رقم 19991 هي عمل ملخص صالح لمقاربة النمط الثاني لتعريف الأخطاء ذات المعنى.

انقل كل رموز الخطأ إلى مكان آخر حتى لا تضطر إلى إلقاء نظرة عليها

lpar ، إذا كنت ترد على وجهة نظري حول نقل معالجة الأخطاء إلى اليمين: هناك فرق كبير بين الحواشي السفلية / التعليقات الختامية (Java) والملاحظات الجانبية (اقتراحي). لا تتطلب الملاحظات الجانبية سوى تغيير بسيط للعين ، دون فقدان السياق.

@ gdm85

ينتهي بك الأمر بالبحث في النص في شفرة المصدر

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

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

يبدو أن هناك وصمة عار حول كل ما يتعلق بجافا وغالبًا ما لا يقدم الناس أي حجج. إنه أمر سيء لمجرد. أنا لست من محبي Java ولكن هذا النوع من التفكير لا يساعد أي شخص.

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

قد يكون هذا غير مفهوم عندما تعطل اللغات التحكم في التدفق نتيجة لخطأ عن طريق تفكيك المكدس وفقدان ذاكرة كل ما فعلوه قبل حدوث الخطأ. الأخطاء مفيدة بالفعل في وقت التشغيل في Go ؛ لا أفهم لماذا يجب أن يحملوا أشياء مثل أرقام الأسطر - تشغيل الكود بالكاد يهتم بذلك.

@كما

مرة أخرى ، الأخطاء ليست للمطور لإصلاح الأخطاء

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

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

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

قد يكون هذا غير مفهوم عندما تعطل اللغات التحكم في التدفق نتيجة لخطأ عن طريق تفكيك المكدس وفقدان ذاكرة كل ما فعلوه قبل حدوث الخطأ

قد يكون لشخص ما ، وليس لي.

الأخطاء مفيدة بالفعل في وقت التشغيل في Go ؛ لا أفهم لماذا يجب أن يحملوا أشياء مثل أرقام الأسطر - تشغيل الكود بالكاد يهتم بذلك.

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

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

هذا خاطئ تمامًا وبشكل مطلق.

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

إذا كان هذا غير مفهوم ، فمن المحتمل أنك تعيش في عالم مختلف تمامًا حيث لا يوجد تتبع.

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

إذا لم تكن بحاجة إلى هذا النوع من المعلومات فتجاهله. لا يؤذيك.

سخام البرمجيات هو سبب إعادة كتابة الخوادم في Go. ما لا تراه يمكن أن يقلل من إنتاجية خط الأنابيب الخاص بك.

أفضل أمثلة على البرامج الفعلية التي تستفيد من وجود هذه الميزة بدلاً من درس غير ذي صلة حول التعامل مع مهلات TCP وإغراق السجل.

تتبع المكدس يكون أبطأ

بالنظر إلى أن تتبعات المكدس يتم إنشاؤها في مسار الخطأ ، فلا أحد يهتم بمدى بطئها. تم بالفعل مقاطعة التشغيل العادي للبرنامج.

وغير مناسب للاستخدام خارج مشاريع الألعاب لأسباب أمنية

حتى الآن لا يزال يتعين عليّ رؤية نظام إنتاج واحد يقوم بإيقاف تشغيل تتبع المكدس بسبب "أسباب أمنية" ، أو على الإطلاق لهذه المسألة. من ناحية أخرى ، فإن القدرة على تحديد المسار الذي سلكته الشفرة بسرعة لإنتاج خطأ كانت مفيدة للغاية. وهذا للمشاريع الكبيرة ، مع العديد من الفرق المختلفة التي تعمل على قاعدة التعليمات البرمجية ، ولا أحد لديه معرفة كاملة بالنظام بأكمله.

ما لا تراه يمكن أن يقلل من إنتاجية خط الأنابيب الخاص بك.

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

بالنظر إلى أن تتبعات المكدس يتم إنشاؤها في مسار الخطأ ، فلا أحد يهتم بمدى بطئها. تم بالفعل مقاطعة التشغيل العادي للبرنامج.

غير صحيح.

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

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

@كما

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

أنا آسف ولكن هذه جملة لا معنى لها ولا علاقة لها بما قلته. لن تجيب عليه.

تتبع المكدس يكون أبطأ

أبطأ ولكن كم؟ هل يهم؟ لا أعتقد ذلك. تطبيقات Go ملزمة IO بشكل عام. المطاردة حول دورات وحدة المعالجة المركزية أمر سخيف في هذه الحالة. لديك مشاكل أكبر بكثير في وقت تشغيل Go والتي تستهلك وحدة المعالجة المركزية. إنها ليست حجة للتخلص من ميزة مفيدة تساعد في إصلاح الأخطاء.

غير مناسب للاستخدام خارج مشاريع الألعاب لأسباب أمنية.

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

إذا كنت تصر على أسباب أمنية ، أود أن تفكر في macOS / iOS ، على سبيل المثال. ليس لديهم مشكلة في إلقاء الذعر وتفريغ الأعطال التي تحتوي على أكوام من جميع مؤشرات الترابط وقيم جميع سجلات وحدة المعالجة المركزية. لا تراهم يتأثرون بهذه "الأسباب الأمنية".

إنه لخطأ تقني أن تجعلهم مواطنين من الدرجة الأولى لمجرد المساعدة في استراتيجيات انتشار الخطأ التي لا تفكر فيها.

هل يمكن أن تكون أكثر ذاتية؟ "استراتيجيات انتشار الخطأ الطائش" أين رأيت ذلك؟

سخام البرمجيات هو سبب إعادة كتابة الخوادم في Go. ما لا تراه يمكن أن يقلل من إنتاجية خط الأنابيب الخاص بك.

مرة أخرى ، بكم؟

أفضل أمثلة على البرامج الفعلية التي تستفيد من وجود هذه الميزة بدلاً من درس غير ذي صلة حول التعامل مع مهلات TCP وإغراق السجل.

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

وجود الكثير من المناقشات غير المثمرة دون أي إجماع يعني أنه لا توجد طريقة أنيقة لحل هذه المشكلة.

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

تضمين التغريدة
يفيد التتبع جميع البرامج ، لكنه يعتمد على ما تقوم بتتبعه. في معظم مشاريع Go التي شاركت فيها ، لم تكن فكرة تتبع المكدس فكرة رائعة ، لأن التزامن متضمن. البيانات تتحرك ذهابًا وإيابًا ، والكثير من الأشياء تتواصل مع بعضها البعض وبعض goroutines ليست سوى بضعة أسطر من التعليمات البرمجية. وجود تتبع المكدس في مثل هذه الحالة لا يساعد.
لهذا السبب أستخدم أخطاء ملفوفة بمعلومات سياق مكتوبة في السجل لإعادة إنشاء تتبع المكدس نفسه ، ولكنه مرتبط ليس بمكدس goroutine الفعلي ، ولكن منطق التطبيق نفسه.
حتى أتمكن من عمل cat * .log | grep "orderID = xxx" واحصل على تتبع المكدس للتسلسل الفعلي للإجراءات التي أدت إلى حدوث خطأ.
نظرًا للطبيعة المتزامنة لأخطاء Go الغنية بالسياق ، فهي أكثر قيمة من تتبع المكدس.

gladkikhartem أشكرك على الوقت الذي

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

تضمين التغريدة

تخيل كود إنتاج حقيقي. كم عدد الأخطاء التي تتوقع أن تولدها؟ [...] ، نظرًا لأن الغالبية العظمى من تطبيقات Go مقيدة بـ IO حتى أن ذلك لن يمثل مشكلة خطيرة.

من الجيد أن تذكر عمليات IO المقيدة. تقوم طريقة io.Reader Read بإرجاع خطأ سليم في EOF. لذلك سيحدث هذا كثيرًا في الطريق السعيد.

urandom

تكشف تتبع المكدس بشكل لا إرادي معلومات قيّمة لتشكيل نظام.

  • أسماء المستخدمين
  • مسارات نظام الملفات
  • نوع / إصدار قاعدة البيانات الخلفية
  • تدفق المعاملات
  • هيكل الكائن
  • خوارزميات التشفير

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

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

تضمين التغريدة

سأتجنب إعطائك أمثلة تسبب المزيد من التنافر. أنا آسف لأنني أحبطتك.

أود أن أقترح استخدام كلمة رئيسية جديدة "returnif" والتي يكشف الاسم عن وظيفتها على الفور. كما أنه مرن بدرجة كافية بحيث يمكن استخدامه في حالات استخدام أكثر من معالجة الأخطاء.

مثال 1 (باستخدام الإرجاع المسمى):

أ ، يخطئ = شيء (ب)
إذا أخطأت! = لا شيء {
إرجاع
}

قد يصبح:

أ ، يخطئ = شيء (ب)
returnif يخطئ! = لا شيء

مثال 2 (عدم استخدام اسم إرجاع):

أ ، يخطئ: = شيء (ب)
إذا أخطأت! = لا شيء {
العودة أ ، يخطئ
}

قد يصبح:

أ ، يخطئ: = شيء (ب)
returnif يخطئ! = لا شيء {أ ، يخطئ}

فيما يتعلق بمثال الإرجاع المسمى ، هل تقصد ...

a, err = something(b)
returnif err != nil

تضمين التغريدة
لماذا لا تقوم فقط بتحديث أداة fmt بدلاً من ذلك وليس عليك تحديث بنية اللغة وإضافة كلمات رئيسية جديدة غير مفيدة

a, err := something(b)
if err != nil { return a, err }

أو

a, err := something(b)
    if err != nil { return a, err }

gladkikhartem الفكرة هي عدم كتابة ذلك في كل مرة تريد فيها نشر الخطأ ، أفضل هذا ويجب أن يعمل بنفس الطريقة

a, err? := something(b)

تضمين التغريدة
الفكرة هي جعل الكود أكثر قابلية للقراءة . كتابة التعليمات البرمجية ليست مشكلة ، القراءة.

يستخدم gladkikhartem rust هذا النهج ولا أعتقد أنه يجعله أقل قابلية للقراءة

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

تضمين التغريدة
وأعتقد أنه يجعله أقل قابلية للقراءة ، فماذا بعد؟ نحاول إيجاد الحل الأفضل أم مجرد مشاركة الآراء؟

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

كما قلت سابقًا ، إذا كنت تريد التخلص من الخطأ! = لا يمكن للمترجم أن يكتشف معلمات الخطأ غير المستخدمة ويعيد توجيهها بنفسه.
و

a, err? := doStuff(a,b)
err? := doAnotherStuff(b,z,d,g)
a, b, err? := doVeryComplexStuff(b)

سيصبح أكثر قابلية للقراءة

a := doStuff(a,b)
doAnotherStuff(b,z,d,g)
a, b := doVeryComplexStuff(b)

نفس السحر ، فقط عدد أقل من الأشياء للكتابة وقليل من الأشياء التي يجب التفكير فيها

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

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

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

ماذا لو تسبب || في ظهور خطأ جديد يلتف تلقائيًا للخطأ الأصلي. لذلك يصبح المثال هكذا

func Chdir(dir string) error {
    syscall.Chdir(dir) || &PathError{"chdir", dir}
    return nil
}

يختفي err ويتم التعامل مع جميع عمليات التغليف ضمنًا. الآن نحن بحاجة إلى طريقة للوصول إلى تلك الأخطاء الداخلية. أعتقد أن إضافة طريقة أخرى مثل Inner() error إلى واجهة error لن تعمل. إحدى الطرق هي تقديم وظيفة مضمنة مثل unwrap(error) []error . ما يفعله هو إرجاع شريحة من جميع الأخطاء الداخلية بالترتيب الذي تم تغليفها به. بهذه الطريقة يمكنك الوصول إلى أي خطأ داخلي أو نطاق فوقها.

يعد تنفيذ هذا أمرًا مشكوكًا فيه نظرًا لأن error هو مجرد واجهة وتحتاج إلى مكان تضع فيه تلك الأخطاء المغلفة لأي نوع error مخصص.

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

هل الاقتراح الأصلي يتعلق بتقليل عدد عمليات التحقق من الأخطاء أو طول كل فحص على حدة؟

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

لقد خدمت التغييرات اللغوية المقترحة حتى الآن كتجربة فكرية مثيرة للاهتمام ولكن لم يكن أي منها واضحًا أو واضحًا أو بسيطًا مثل الأصل. Go ليس bash ولا شيء يجب أن يكون "سحريًا" بشأن الأخطاء.

قرأت هذه المقترحات أكثر فأكثر ، وأرى المزيد والمزيد من الناس الذين الحجج ليست سوى "يضيف شيئًا جديدًا ، لذا فهو سيء وغير قابل للقراءة ، اترك كل شيء كما هو".

@ كما يعطي الاقتراح ملخصًا لما يحاول تحقيقه. ما يتم فعله محدد جيدًا.

واضح أو واضح أو بسيط مثل الأصل

سيقدم أي اقتراح بناء جملة جديدًا ، كما أن كونه جديدًا بالنسبة لبعض الأشخاص يبدو هو نفسه "غير قابل للقراءة ، ومعقد ، إلخ". لمجرد أنها جديدة لا تجعلها أقل وضوحًا أو وضوحًا أو سهولة في النطق. "||" و "؟" الأمثلة واضحة وبسيطة مثل بناء الجملة الحالي بمجرد أن تعرف ما يفعله. أم يجب أن نبدأ بالشكوى من أن "->" و "<-" ساحرتان للغاية ويجب على القارئ أن يعرف ما تعنيه؟ دعنا نستبدلها باستدعاءات الطريقة.

Go ليس bash ولا شيء يجب أن يكون "سحريًا" بشأن الأخطاء.

هذا لا أساس له على الإطلاق ولا يعتبر حجة لأي شيء. ما علاقة ذلك بباش هو أمر بعيد عني.

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

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

الأصل || الاقتراح جميل جدًا ، وتم اختباره عمليًا ، لكن التنسيق قبيح في رأيي ، أقترح تغيير التنسيق إلى

syscal.Chdir(dir)
    || return &PathError{"chdir", dir}

ما رأيك في مثل هذا البديل من بناء الجملة السحري؟

syscal.Chdir(dir) {
    return &PathError{"chdir", dir}
}

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

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

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath, err := p.preparePath()
        if err != nil {
                return nil, err
        }
    fis, err := l.context.ReadDir(absPath)
    if err != nil {
        return nil, err
    } else if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

كيف التغييرات المقترحة تنظف هذه الوظيفة؟

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath, err? := p.preparePath()
    fis, err? := l.context.ReadDir(absPath)
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
         if _, ok := err.(*build.NoGoError); ok {
             // There isn't any Go code here.
             return nil, nil
         }
         return nil, err
    }

    return buildPkg, nil
}

يناسب اقتراح bcmills بشكل أفضل.

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
    absPath := p.preparePath()        =? err { return nil, err }
    fis := l.context.ReadDir(absPath) =? err { return nil, err }
    if len(fis) == 0 {
        return nil, nil
    }
    buildPkg := l.context.Import(".", absPath, 0) =? err {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }
    return buildPkg, nil
}

@ object88

func (l *Loader) processDirectory(p *Package) (p *build.Package, err error) {
        absPath, err := p.preparePath() {
        return nil, fmt.Errorf("prepare path: %v", err)
    }
    fis, err := l.context.ReadDir(absPath) {
        return nil, fmt.Errorf("read dir: %v", err)
    }
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0) {
        err, ok := err.(*build.NoGoError)
                if !ok {
            return nil, fmt.Errorf("buildpkg: %v",err)
        }
        return nil, nil
    }
    return buildPkg, nil
}

الاستمرار في الوخز في هذا. ربما يمكننا التوصل إلى أمثلة أكثر اكتمالا للاستخدام.

erwbgy ، هذا يبدو الأفضل بالنسبة لي ، لكنني لست مقتنعًا بأن العائد رائع.

  • ما هي قيم العودة غير الخطأ؟ هل هم دائما القيمة الصفرية؟ إذا كانت هناك _was_ قيمة تم تعيينها مسبقًا لعائد مسمى ، فهل يتم تجاوز ذلك؟ إذا تم استخدام القيمة المسماة لتخزين نتيجة دالة خطأ ، فهل يتم إرجاعها؟
  • هل يمكن تطبيق عامل التشغيل ? على قيمة غير خطأ؟ هل يمكنني أن أفعل شيئًا مثل (!ok)? أو ok!? (وهو أمر غريب بعض الشيء ، لأنك تجمع المهمة والعملية)؟ أم أن بناء الجملة هذا لا يصلح إلا لـ error ؟

rodcorsi ، هذا يزعجني لأنه ماذا لو لم تكن الوظيفة ReadDir ولكن ReadBuildTargetDirectoryForFileInfo أو شيء سخيف طويل كهذا. أو ربما لديك عدد كبير من الحجج. قد يتم أيضًا دفع معالجة الخطأ لـ preparePath بعيدًا عن الشاشة. على جهاز بحجم شاشة أفقي محدود (أو منفذ عرض ليس بهذا العرض ، مثل Github) ، من المحتمل أن تفقد الجزء =? . نحن بارعون جدًا في التمرير العمودي ؛ ليس كثيرًا في الوضع الأفقي.

gladkikhartem ، يبدو أنه مرتبط ببعض الحجة (الأخيرة فقط؟) التي تطبق واجهة error . إنه يشبه إلى حد كبير إعلان الوظيفة ، وهذا ... _ يشعر_ بالغرابة. هل هناك طريقة ما يمكن من خلالها ربطه ok عائد على النمط

@ object88
التفاف الكلمات يحل مشاكل التعليمات البرمجية الواسعة حقًا. هو لا يستخدم على نطاق واسع؟

@ object88 بخصوص استدعاء دالة طويل جدا. دعونا نتعامل مع القضية الرئيسية هنا. المشكلة ليست في معالجة الخطأ الذي تم دفعه خارج الشاشة. المشكلة هي اسم وظيفة طويلة و / أو قائمة كبيرة من الوسائط. يجب إصلاح ذلك قبل إجراء أي حجة حول معالجة الخطأ خارج الشاشة.

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

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

@ object88
نعم ، سيعمل هذا الرمز مع أي وظيفة تُرجع واجهة الخطأ كمعامل أخير.

فيما يتعلق بحفظ الخط ، لا يمكنك وضع المزيد من المعلومات في عدد أقل من الأسطر. يجب توزيع الكود بشكل متساوٍ ، وليس كثيفًا جدًا ولا يحتوي على مساحة بعد كل عبارة.
أوافق على أنه يبدو كإعلان عن وظيفة ، لكنه في نفس الوقت مشابه جدًا للإعلان الحالي إذا ... ؛ err! = لا شيء {بيان ، لذلك لن يكون الناس في حيرة من أمرهم.

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

فقط من أجل الاكتمال ، سأقدم مثالاً مع بناء الجملة || ، التفاف تلقائي للخطأ والتصفية التلقائية لقيم الإرجاع غير الخطأ

func (l *Loader) processDirectory(p *Package) (*build.Package, error) {
        absPath := p.preparePath() || errors.New("prepare path")
    fis := l.context.ReadDir(absPath) || errors.New("ReadDir")
    if len(fis) == 0 {
        return nil, nil
    }

    buildPkg, err := l.context.Import(".", absPath, 0)
    if err != nil {
        if _, ok := err.(*build.NoGoError); ok {
            // There isn't any Go code here.
            return nil, nil
        }
        return nil, err
    }

    return buildPkg, nil
}

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

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

err := func()
if err != nil {
    return err
}

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

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

@ object88

الادعاء هو أن هناك "الكثير من التعليمات البرمجية" حول الأخطاء.

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

تضمين التغريدة
أوافق على أنه إذا كان err != nil return err كثير جدًا ، فإن الشيء الذي نخشاه هو أننا إذا أنشأنا طريقة سهلة لإعادة توجيه الخطأ إلى المكدس - سيستخدم المبرمجون الإحصائيون ، وخاصة الصغار ، أسهل طريقة ، بدلاً من القيام بذلك ما هو مناسب في موقف معين.
إنها نفس الفكرة مع معالجة أخطاء Go - فهي تجبرك على فعل شيء لائق.
لذا ، في هذا الاقتراح ، نريد تشجيع الآخرين على التعامل مع الأخطاء ولفها بعناية.

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

gladkikhartem لطالما وجدت هذه الحجة حول المحررين القدامى سخيفة. من يهتم بذلك ولماذا تعاني اللغة من ذلك؟ إنه 2018 ، كل شخص لديه شاشة عرض كبيرة ومحرر لائق. يجب ألا تؤثر الأقلية الصغيرة جدًا على أي شخص آخر. يجب أن يكون العكس هو الصحيح - على الأقلية أن تتعامل معها بنفسها. التمرير ، التفاف الكلمات ، أي شيء.

لدىgladkikhartem Go بالفعل هذه المشكلة ولا أعتقد أنه يمكننا فعل أي شيء حيال ذلك. سيكون المطورون دائمًا كسالى حتى تجبرهم على فشل التجميع أو الذعر في وقت التشغيل والذي ، بالمناسبة ، لا يفعل Go.

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

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

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

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

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

Go لا يفرض معالجة الأخطاء ، كما يفعل linters. (ربما يجب علينا فقط كتابة لينتر لهذا؟)

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

فمثلا:

  1. يجب أن يحتوي بناء جملة الاقتراح الجديد على بيان عودة في النص ، وإلا فلن يكون واضحًا للقارئ ما يحدث. ( موافق غير موافق )
  2. يجب أن يدعم الاقتراح الجديد الوظائف التي تُرجع قيمًا متعددة (موافق / غير موافق)
  3. يجب أن يأخذ الاقتراح الجديد مساحة أقل (سطر واحد ، سطران ، غير موافق)
  4. يجب أن يكون الاقتراح الجديد قادرًا على التعامل مع التعبيرات الطويلة جدًا (موافق / غير موافق)
  5. يجب أن يسمح الاقتراح الجديد ببيانات متعددة في حالة الخطأ (موافق / غير موافق)
  6. .....

creker ، يتم تنفيذ حوالي 75٪ من تطوري على كمبيوتر محمول بحجم 15 بوصة في VSCode. أقوم بتعظيم العقارات الأفقية الخاصة بي ، ولكن لا يزال هناك حد ، خاصة إذا كنت أقوم بالتعديل جنبًا إلى جنب. سأراهن على ذلك بين الطلاب ، هناك أجهزة كمبيوتر محمولة أكثر بكثير من أجهزة الكمبيوتر المكتبية. لا أريد الحد من سهولة الوصول إلى اللغة لأننا نتوقع أن يكون لدى الجميع شاشات كبيرة الحجم.

ولسوء الحظ ، بغض النظر عن حجم الشاشة لديك ، لا يزال جيثب يحد من منفذ العرض.

تضمين التغريدة

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

ليس من الواضح ما الذي يشكل simplifying error handling ، لكن السابقة هي أن less runes != simple . أعتقد أن هناك عددًا قليلاً من المؤهلات للبساطة التي يمكنها قياس بناء بطريقة قابلة للقياس الكمي:

  • عدد الطرق التي يتم التعبير عنها بالبناء
  • تشابه هذا البناء مع البناء الآخر ، والتماسك بين تلك البنى
  • عدد العمليات المنطقية التي لخصها البناء
  • تشابه البناء مع اللغة الطبيعية (على سبيل المثال ، غياب النفي ، إلخ)

على سبيل المثال ، يزيد الاقتراح الأصلي من عدد طرق نشر الأخطاء من 2 إلى 3. وهو مشابه لـ OR المنطقي ، لكن له دلالات مختلفة. وهي تلخص العائد المشروط للتعقيد المنخفض (مقارنة بالقول copy أو append ، أو >> ). الطريقة الجديدة أقل طبيعية من الطريقة القديمة ، وإذا تم التحدث بها بصوت عالٍ ، فمن المحتمل أن تكون abs, err := path(foo) || return err -> if theres an error, it's returning err في هذه الحالة سيكون لغزا سبب إمكانية استخدام الأشرطة الرأسية إذا كنت يمكن كتابتها بنفس الطريقة التي قيلت بها بصوت عالٍ في مراجعة التعليمات البرمجية.

@كما
أوافق تمامًا على أن less runes != simple .
بكل بساطة أعني أنه مقروء ومفهوم.
حتى يتمكن أي شخص ليس على دراية بـ go من قراءته وفهم ما يفعله.
يجب أن تكون مزحة - لست مضطرًا لشرحها.

معالجة الأخطاء الحالية أمر مفهوم بالفعل ، ولكن لا يمكن قراءته تمامًا إذا كان لديك الكثير من if err != nil return.

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

يقترحونgladkikhartem لا ، لا

أوافق على أنه يجب علينا صياغة ما نريد بشكل أفضل لأن الاقتراح لا يغطي جميع الجوانب بالكامل.

@كما

الطريقة الجديدة أقل طبيعية من الطريقة القديمة ، وإذا تم التحدث بها بصوت عالٍ فمن المحتمل أن تكون abs ، يخطئ: = المسار (foo) || إرجاع الخطأ -> إذا كان هناك خطأ ، فسيتم إرجاع الخطأ وفي هذه الحالة سيكون لغزا لماذا من الممكن استخدام الأشرطة الرأسية إذا كان بإمكانك كتابتها بنفس الطريقة التي قيلت بها بصوت عالٍ في مراجعة الكود.

الطريقة الجديدة أقل طبيعية لسبب واحد فقط - إنها ليست جزءًا من اللغة في الوقت الحالي. ليس هناك سبب آخر. تخيل الذهاب بالفعل مع بناء الجملة - سيكون ذلك طبيعيًا لأنك معتاد عليه. تمامًا كما لو كنت على دراية بـ -> و select و go وأشياء أخرى غير موجودة بلغات أخرى. لماذا من الممكن استخدام أشرطة عمودية بدلاً من العودة؟ أنا أجيب بسؤال. لماذا توجد طريقة لإلحاق الشرائح في مكالمة واحدة بينما يمكنك فعل الشيء نفسه باستخدام التكرار الحلقي؟ لماذا توجد طريقة لنسخ الأشياء من واجهة قارئ إلى كاتب في مكالمة واحدة بينما يمكنك فعل الشيء نفسه باستخدام حلقة؟ إلخ إلخ ، لأنك تريد أن يكون الرمز الخاص بك أكثر إحكاما وقابلية للقراءة. أنت تقدم هذه الحجج في حين أن Go يناقضها بالفعل مع العديد من الأمثلة. مرة أخرى ، لنكن أكثر انفتاحًا ولا نسقط أي شيء لمجرد أنه جديد وليس في اللغة بالفعل. لن نحقق أي شيء مع ذلك. هناك مشكلة ، كثير من الناس يطالبون بحل ، فلنتعامل معها. إن Go ليست لغة مقدسة مثالية سيتم تدنيسها بأي شيء يضاف إليها.

لماذا توجد طريقة لإلحاق الشرائح في مكالمة واحدة بينما يمكنك فعل الشيء نفسه باستخدام التكرار الحلقي؟

كتابة التحقق من خطأ العبارة أمر تافه ، سأكون مهتمًا برؤية تنفيذك لـ append .

لماذا توجد طريقة لنسخ الأشياء من واجهة قارئ إلى كاتب في مكالمة واحدة بينما يمكنك فعل الشيء نفسه باستخدام حلقة؟

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

أنت تقدم هذه الحجج في حين أن Go يناقضها بالفعل مع العديد من الأمثلة.

لا أعتقد أن هذا هو الحال ، على الأقل ليس مع تلك الأمثلة.

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

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

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

فيما يتعلق بشيء الشاشة العريضة مقابل الشاشة الرفيعة ، هناك شيء آخر يجب مراعاته وهو تعدد المهام .

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

@كما
أوافق تمامًا على أن معظم الميزات المقترحة غير عملية ، وبدأت أعتقد ذلك || و ؟ يمكن أن يكون الأمر كذلك.

تضمين التغريدة
copy () و append () ليستا مهام بسيطة للتنفيذ

لدي لينتات على CI / CD وهي تجبرني حرفيًا على معالجة جميع الأخطاء. إنهم ليسوا جزءًا من اللغة ، لكن هذا لا يهم - أنا فقط بحاجة إلى نتائج.
(وبالمناسبة ، لدي رأي قوي - إذا كان شخص ما لا يستخدم linters في Go - فهو فقط ........)

حول حجم الشاشة - إنها ليست مضحكة ، بجدية. من فضلك توقف عن هذا النقاش غير ذي الصلة. يمكن أن تكون شاشتك بالعرض الذي تريده - سيكون لديك دائمًا احتمال ألا يكون جزء من الكود || return &PathError{Err:err} مرئيًا. ما عليك سوى البحث عن كلمة "بيئة عمل متكاملة" على google وشاهد نوع المساحة المتوفرة للتعليمات البرمجية.

ويرجى قراءة نص الآخرين بعناية ، لم أقل أن Go يجبرك على التعامل مع جميع الأخطاء

إنها نفس الفكرة مع معالجة أخطاء Go - فهي تجبرك على فعل شيء لائق.

gladkikhartem Go لا يفرض أي شيء فيما يتعلق بمعالجة الأخطاء ، هذه هي المشكلة. شيء لائق أم لا ، لا يهم ، هذا مجرد اختيار. على الرغم من أن هذا يعني بالنسبة لي معالجة جميع الأخطاء في جميع الحالات باستثناء أشياء مثل fmt.Println .

إذا كان شخص ما لا يستخدم linters في Go - فهو فقط

ربما يكون. ولكن إذا لم يتم إجبار شيء ما ، فلن يطير. البعض سيستخدمها والبعض الآخر لن يستخدمها.

حول حجم الشاشة - إنها ليست مضحكة ، بجدية. من فضلك توقف عن هذا النقاش غير ذي الصلة.

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

إذا كنا نتحدث عن حجم شاشتي. يمنحني رمز الاستوديو المرئي 270 رمزًا للمساحة الأفقية. لن أدافع عن أنه من الطبيعي أن تأخذ هذه المساحة الكبيرة. لكن يمكن أن يتجاوز الكود الخاص بي بسهولة 120 رمزًا عندما تأخذ في الاعتبار الهياكل ذات التعليقات وأنواع الحقول ذات الأسماء الطويلة بشكل خاص. إذا كنت سأستخدم بناء الجملة || ، فسوف يتناسب بسهولة مع 100-120 في حالة استدعاء دالة وسيطة من 3 إلى 5 وخطأ ملفوف مع رسالة مخصصة.

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

erwbgy ، هذا يبدو الأفضل بالنسبة لي ، لكنني لست مقتنعًا بأن العائد رائع.

@ object88 العائد بالنسبة لي هو أنه يزيل

val, err := func()
if err != nil {
    return nil, errors.WithStack(err)
}

أبسط:

val, err? := func()

لا شيء يمنع معالجة الأخطاء الأكثر تعقيدًا بالطريقة الحالية.

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

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

يمكن لل ؟ يتم تطبيق عامل التشغيل على قيمة غير خطأ؟ هل يمكنني فعل شيء مثل (! حسنًا)؟ أو طيب !؟ (وهو أمر غريب بعض الشيء ، لأنك تجمع بين المهام والعملية)؟ أم أن بناء الجملة هذا جيد للخطأ فقط؟

لا ، لا أعتقد أنه من المنطقي استخدام بناء الجملة هذا لأي شيء بخلاف قيم الخطأ.

أعتقد أن وظائف "must" تتكاثر بدافع اليأس للحصول على تعليمات برمجية أكثر قابلية للقراءة.

sqlx

db.MustExec(schema)

نموذج html

var t = template.Must(template.New("name").Parse("html"))

أقترح عامل تشغيل الذعر (لست متأكدًا مما إذا كان يجب أن أسميها "مشغل")

a,  😱 := someFunc(b)

نفس الشيء ، ولكن ربما أكثر فورية من

a, err := someFunc(b)
if err != nil {
  panic(err)
}

😱 ربما يكون من الصعب جدًا كتابته ، فيمكننا استخدام شيء مثل! ، أو !! ، أو

a,  !! := someFunc(b)
!! = maybeReturnsError()

يمكن !! الذعر و! عائدات

حان وقت 2 سنت. لماذا لا يمكننا فقط استخدام debug.PrintStack() للمكتبة القياسية لتتبع المكدس؟ الفكرة هي طباعة تتبع المكدس فقط في المستوى الأعمق ، حيث حدث الخطأ.

لماذا لا يمكننا فقط استخدام debug.PrintStack() للمكتبة القياسية لتتبع المكدس؟

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

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

ما هو الأسلوب الأكثر سهولة في الاستخدام للمبتدئين في البرمجة الكاملة؟

وجدت نسخة أبسط. يحتاج فقط if !err
لا شيء خاص ، بديهي ، بدون علامات ترقيم إضافية ، كود أصغر بكثير

"اذهب
absPath ، يخطئ: = p.preparePath ()
العودة لا شيء ، يخطئ إذا أخطأ

يخطئ: = doSomethingWith (absPath) إذا! يخطئ
doSomethingElse () إذا! يخطئ

doSomethingRegardlessOfErr ()

// معالجة الأخطاء في مكان واحد ؛ إذا لزم الأمر اصطياد دون مسافة بادئة
إذا أخطأت {
إرجاع "خطأ بدون تلوث رمز" ، يخطئ
}
""

err := doSomethingWith(absPath) if !err
doSomethingElse() if !err

مرحبًا بكم مرة أخرى ، شروط نشر MUMPS القديمة الجيدة ؛-)

شكرا ولكن لا شكرا.

dmajkic هذا لا يفعل أي شيء للمساعدة في "إرجاع الخطأ بمعلومات سياقية إضافية".

erwbgy ، عنوان هذه المشكلة هو _proposal: Go 2: تبسيط معالجة الأخطاء مع || يخطئ لاحقة_ كان تعليقي في هذا السياق. آسف إذا دخلت في المناقشة السابقة.

تضمين التغريدة ظروف ما بعد ليست Go-way ، لكن الظروف المسبقة تبدو ملوثة أيضًا:

if !err; err := doSomethingWith(absPath)
if !err; doSomethingElse()

dmajkic هناك ما هو أكثر من مجرد العنوان - يصف ianlancetaylor ثلاث طرق لمعالجة الأخطاء ويشير على وجه التحديد إلى أن القليل من المقترحات تجعل من السهل إرجاع الخطأ بمعلومات إضافية.

لقد مررتerwbgy بكل المشكلات التي حددها ianlancetaylor ، try() ) أو استخدام أحرف خاصة غير رقمية. أنا شخصياً - لا أحب ذلك ، لأن الكود مثقل بـ! "# $٪ ويميل إلى أن يبدو مسيئًا ، مثل الشتائم.

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

ماذا عن المؤجل الشرطي

func something() (int, error) {
    var error err
    var oth err

    defer err != nil {
        return 0, mycustomerror("More Info", err)
    }
    defer oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }

    _, err = a()
    _, err = b()
    _, err = c()
    _, oth = d()
    _, err = e()

    return 2, nil
}


func something() (int, error) {
    var error err
    var oth err

    _, err = a()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = b()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, err = c()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }
    _, oth = d()
    if oth != nil {
        return 1, mycustomerror("Some other case", oth)
    }
    _, err = e()
    if err != nil {
        return 0, mycustomerror("More Info", err)
    }

    return 2, nil
}

سيغير ذلك بشكل كبير معنى defer - إنه مجرد شيء يتم تشغيله في نهاية النطاق ، وليس شيئًا يتسبب في خروج النطاق مبكرًا.

إذا قاموا بتقديم Try Catch بهذه اللغة ، فسيتم حل كل هذه المشكلات بطريقة سهلة للغاية.

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

try (var err error){
     i, err:=DoSomething1()
     i, err=DoSomething2()
     i, err=DoSomething3()
} catch (err error){
   HandleError(err)
   // return err  // similar to throw err
} finally{
  // Do something
}

image

sbinet هذا أفضل من لا شيء ، لكن إذا استخدموا ببساطة نموذج try-catch نفسه الذي يعرفه الجميع فهو أفضل بكثير.

KamyarM يبدو أنك تقترح إضافة آلية

يشبه Swift الذي يحتوي أيضًا على "استثناءات" لا تعمل تمامًا مثل الاستثناءات.

لقد أظهرت لغات مختلفة أن try catch هو حقًا حل من الدرجة الثانية ، بينما أعتقد أن Go لن يكون قادرًا على حل هذا الأمر باستخدام ربما monad وما إلى ذلك.

ianlancetaylor لقد أشرت للتو إلى Try-Catch بلغات برمجة أخرى مثل C ++ و Java و C # ... وليس الحل الذي

استثناءات Java هي حطام قطار ، لذلك يجب أن أختلف معك بشدة هناKamyarM. فقط لأن شيئًا ما مألوفًا ، لا يعني أنه اختيار جيد.

الذي أقصده.

KamyarM شكرا للتوضيح. نظرنا صراحة في الاستثناءات ورفضناها. الأخطاء ليست استثنائية. تحدث لجميع أنواع الأسباب الطبيعية تمامًا. https://blog.golang.org/errors-are-values

استثنائية أم لا لكنها تحل مشكلة سخام الكود بسبب خطأ في معالجة النموذج المعياري. نفس المشكلة أصابت Objective-C بالشلل والتي تعمل تمامًا مثل Go. الأخطاء هي مجرد قيم من نوع NSError ، ولا يوجد شيء مميز عنها. ولها نفس المشكلة مع الكثير من ifs وتغليف الخطأ. وهذا هو سبب تغيير Swift للأشياء. انتهى بهم الأمر بمزيج من اثنين - إنه يعمل مثل الاستثناءات مما يعني أنه ينهي التنفيذ ويجب عليك التقاط الاستثناء. لكنها لا تزيل المكدس وتعمل مثل الإرجاع المنتظم. لذا فإن الحجة الفنية ضد استخدام الاستثناءات للتحكم في التدفق لا تنطبق هناك - فهذه "الاستثناءات" بنفس سرعة العائد المنتظم. إنه أكثر من سكر نحوي. لكن Swift لديها مشكلة فريدة معهم. العديد من واجهات برمجة تطبيقات Cocoa غير متزامنة (عمليات الاسترجاعات و GCD) وغير متوافقة مع هذا النوع من معالجة الأخطاء - الاستثناءات غير مجدية بدون شيء مثل الانتظار. ولكن إلى حد كبير كل كود Go متزامن ويمكن لهذه "الاستثناءات" أن تعمل بالفعل.

urandom
الاستثناءات في Java ليست سيئة. تكمن المشكلة في المبرمجين السيئين الذين لا يعرفون كيفية استخدامها.

إذا كانت لغتك بها ميزات رهيبة ، فسيستخدم شخص ما هذه الميزة في النهاية. إذا كانت لغتك لا تحتوي على مثل هذه الميزة ، فهناك فرصة 0٪. إنها رياضيات بسيطة.

@ كما لا أتفق معك في أن try-catch ميزة رهيبة. إنها ميزة مفيدة للغاية وتجعل حياتنا أسهل بكثير وهذا هو سبب تعليقنا هنا ، لذلك قد يضيف فريق Google GoLang وظائف مماثلة. أنا شخصياً أكره رموز معالجة أخطاء if-elses في GoLang ولا أحب مفهوم استعادة الذعر كثيرًا (إنه مشابه لـ try-catch ولكن ليس منظمًا كما هو الحال مع كتل Try-Catch-Last) . يضيف الكثير من الضوضاء في الكود مما يجعل الكود غير قابل للقراءة في كثير من الحالات.

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

وماذا عن من يأتي من C / C ++ ، Objective-C حيث لدينا نفس المشكلة بالضبط مع boilerplate؟ ومن المحبط رؤية لغة حديثة مثل Go تعاني من نفس المشاكل تمامًا. هذا هو السبب في أن هذا الضجيج الكامل حول الأخطاء كقيم يبدو مزيفًا وسخيفًا للغاية - لقد تم بالفعل لسنوات وعشرات السنين. يبدو الأمر وكأن Go لم يتعلم شيئًا من تلك التجربة. بالنظر بشكل خاص إلى Swift / Rust الذي يحاول بالفعل إيجاد طريقة أفضل. انتقل إلى الحل الحالي مثل Java / C # الذي تم تسويته مع استثناءات ولكن على الأقل هذه اللغات أقدم بكثير.

KamyarM هل سبق لك استخدام البرمجة الموجهة للسكك الحديدية؟ شعاع؟

لن تمدح الاستثناءات كثيرًا ، إذا كنت ستستخدمها ، imho.

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

أحد الأسباب التي تجعل بعض الناس يقولون إن Try-Catch ليس جيدًا بسبب أدائه. من المحتمل أن يكون سبب ذلك هو استخدام جدول مخطط معالج لكل مكان قد يحدث فيه استثناء. قرأت في مكان ما أنه حتى الاستثناءات تكون أسرع (التكلفة الصفرية عندما لا تحدث استثناءات ولكن لها تكلفة أكبر عندما تحدث بالفعل) مقارنتها بـ If Error check (يتم فحصها دائمًا بغض النظر عن وجود خطأ أم لا). بخلاف ذلك ، لا أعتقد أن هناك أي مشكلة في بناء جملة Try-Catch. إن الطريقة التي يطبقها المترجم فقط هي التي تجعلها مختلفة وليس تركيبها.

ShalokShalom الرجاء التحقق من هذا:
https://mortoray.com/2013/09/12/the-true-cost-of-zero-cost-exceptions/

يثني الأشخاص القادمون من C / C ++ تمامًا على عدم وجود استثناءات و
لاتخاذ قرار حكيم ، ومقاومة أولئك الذين يدعون أنه "حديث" و
شكر الله على سير العمل المقروء (خاصة بعد C ++).

يوم الثلاثاء ، 17 أبريل 2018 الساعة 03:46 ، Antonenko Artem [email protected]
كتب:

وماذا عن من يأتي من C / C ++ ، Objective-C حيث لدينا نفس الشيء
مشكلة بالضبط مع المتداول؟ ومن المحبط رؤية لغة حديثة
مثل Go يعانون من نفس المشاكل بالضبط. لهذا السبب كل هذا الضجيج
حول الأخطاء لأن القيم تبدو وهمية وسخيفة - لقد تم بالفعل
لسنوات وعشرات السنين. يبدو أن Go لم يتعلم شيئًا من ذلك
خبرة. بالنظر بشكل خاص إلى Swift / Rust الذي يحاول العثور عليه بالفعل
طريقة أفضل. انتقل مستقرًا مع حل موجود مثل Java / C # تم تسويته مع
استثناءات ولكن هذه على الأقل لغات أقدم بكثير.

-
أنت تتلقى هذا لأنك علقت.
قم بالرد على هذا البريد الإلكتروني مباشرة ، وقم بعرضه على GitHub
https://github.com/golang/go/issues/21161#issuecomment-381793840 ، أو كتم الصوت
الخيط
https://github.com/notifications/unsubscribe-auth/AICzv9w608ea2fwPq_wNpTDBnKMAdAKTks5tpTtsgaJpZM4Oi1c-
.

kirillx لم

يجب على الأشخاص الذين يدافعون عن الاستثناءات قراءة هذا المقال: https://ckwop.me.uk/Why-Exceptions-Suck.html

السبب وراء كون استثناءات أسلوب Java / C ++ سيئة بطبيعتها لا علاقة له بأداء تطبيقات معينة. الاستثناءات سيئة لأنها "عند حدوث خطأ" لـ BASIC ، مع وجود "gotos" غير مرئية في السياق حيث قد تصبح سارية المفعول. الاستثناءات تخفي معالجة الخطأ بعيدًا حيث يمكنك نسيانها بسهولة. كان من المفترض أن تحل استثناءات Java التي تم التحقق منها هذه المشكلة ، لكنها لم تفعل ذلك من الناحية العملية لأن الناس اكتشفوا وأكلوا الاستثناءات أو آثار مكدس ملقاة في كل مكان.

أكتب Java في معظم الأسابيع ، ولا أرغب بالتأكيد في رؤية استثناءات على غرار Java في Go ، بغض النظر عن مدى أدائها العالي.

lpar ليست كل حلقات for ، بينما الحلقات ، if elses ، تبديل الحالات ، فواصل وتستمر في نوع من أشياء GoTo. فماذا تبقى من لغة البرمجة إذن؟

بينما ، من أجل و if / else لا يتضمن تدفق التنفيذ القفز بشكل غير مرئي إلى مكان آخر بدون علامة تشير إلى أنه سيتم.

ما هو الاختلاف إذا قام شخص ما بنقل الخطأ إلى المتصل السابق في GoLang وقام هذا المتصل بإعادة ذلك إلى المتصل السابق وما إلى ذلك (بخلاف الكثير من ضوضاء الكود)؟ ما مقدار الرموز التي نحتاج إلى فحصها واجتيازها لمعرفة من سيتعامل مع الخطأ؟ الشيء نفسه ينطبق مع try-catch.

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

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

كما ترى أدناه ، نحتاج إلى إضافة 4 أسطر من التعليمات البرمجية فقط حتى لا نعالج الخطأ:

func myFunc1() error{
  // ...
  if (err){
      return err
  }
  return nil
}

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

يعتبر:

x, err := lib.SomeFunc(100, 4)
if err != nil {
  // A
}
// B

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

التباين مع Java:

x = SomeFunc(100, 4)

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

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

مع Java ، يمكن أن تحدث هذه المشكلات مع كل مكالمة - ليس فقط مع التعليمات البرمجية السيئة ، إنه شيء يجب أن تقلق بشأنه مع _all_ كود Java. لهذا السبب تحتوي بيئات تطوير Java على مساعدة منبثقة لتظهر لك ما إذا كانت الوظيفة التي تشير إليها قد تتسبب في استثناء أم لا ، وما هي الاستثناءات التي قد تسببها. لهذا السبب أضافت Java استثناءات محددة ، لذلك بالنسبة للأخطاء الشائعة ، يجب أن يكون لديك على الأقل بعض التحذير من أن استدعاء الوظيفة قد يثير استثناءً ويحول تدفق البرنامج. وفي الوقت نفسه ، فإن القيم الخالية التي تم إرجاعها والطبيعة غير المحددة لـ NullPointerException هي مشكلة لدرجة أنهم أضافوا فئة Optional إلى Java 8 لمحاولة تحسينها ، على الرغم من أن التكلفة تضطر إلى التفاف إرجاع القيمة على كل دالة واحدة تقوم بإرجاع كائن.

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

لا أعتقد أن بناء الجملة به أي مشكلة.

لا أعتقد أن أي شخص يقول أن بناء الجملة هو مشكلة استثناءات أسلوب جافا.

lpar ، لماذا لا يوجد فزع من عدم

هل الذعر فقط قادر على التعافي والرميات قابلة للإمساك بها؟ حق؟

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

لماذا لا يوجد ذعر من عدم الرجوع في Go أفضل من NullPointerException في Java؟

لأن الأول يكاد لا يحدث أبدًا في تجربتي ، في حين أن الأخير يحدث طوال الوقت للأسباب التي أوضحتها.

lpar حسنًا ، لم

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

كان الإسناد الفارغ مثالاً سيئًا. إذا لم تلتقطها ، تعمل Go و Java بنفس الطريقة تمامًا - ستحصل على تعطل مع تتبع المكدس. كيف يمكن أن يكون تتبع المكدس عديم الفائدة أنا لست الآن. أنت تعرف بالضبط المكان الذي حدث فيه ذلك. في كل من C # و Go بالنسبة لي ، عادةً ما يكون إصلاح هذا النوع من التعطل أمرًا تافهًا لأن الإسناد الفارغ في تجربتي يرجع إلى خطأ مبرمج بسيط. في هذه الحالة بالذات ، لا يوجد شيء لنتعلمه من أي شخص.

تضمين التغريدة

لأن الأول يكاد لا يحدث أبدًا في تجربتي ، في حين أن الأخير يحدث طوال الوقت للأسباب التي أوضحتها.

هذا عرضي ولم أر أي سبب في تعليقك بأن Java أسوأ بطريقة ما في عدم وجود / خالية من Go. لقد لاحظت عدم وجود العديد من الأعطال في عدم وجود إشارة مرجعية في كود Go. إنها تمامًا نفس الإسناد الفارغ في C # / Java. قد تصادف أنك تستخدم المزيد من أنواع القيم في Go مما يساعد (يحتوي C # أيضًا على هذه الأنواع) ولكنه لا يغير أي شيء.

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

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

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

كيف يمكن أن يكون تتبع المكدس عديم الفائدة أنا لست الآن.

قلت "عديم الجدوى تقريبا". كما هو الحال في ، أحتاج فقط سطرًا واحدًا من المعلومات منه ولكنه يتكون من عشرات الأسطر.

هذا عرضي ولم أر أي سبب في تعليقك بأن Java أسوأ بطريقة ما في عدم وجود / خالية من Go.

إذن أنت لا تستمع. ارجع واقرأ الجزء الخاص بالعقود الضمنية.

يمكن نشر الأخطاء بدون أي عبارات شرطية صريحة.

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

سواء كانت الاستثناءات التي تم تنفيذها بواسطة Rust أو Swift تعاني من نفس مشاكل Java التي لا أعرفها ، سأترك ذلك لشخص لديه خبرة باللغات المعنية.

KamyarM أنت في الأساس تجعل لا شيء غير ضروري وتحصل على أمان كامل لذلك:

https://fsharpforfunandprofit.com/posts/the-option-type/

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

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

قلت "عديم الجدوى تقريبا". كما هو الحال في ، أحتاج فقط سطرًا واحدًا من المعلومات منه ولكنه يتكون من عشرات الأسطر.

وهل تعد آثار Go الضخمة مع مئات من goroutines أكثر فائدة إلى حد ما؟ أنا لا أفهم إلى أين أنت ذاهب مع هذا. Java و Go هما نفس الشيء تمامًا هنا. وأحيانًا تجد أنه من المفيد مراقبة المكدس الكامل لفهم كيف انتهى الأمر بالشفرة الخاصة بك حيث تعطلت. ساعدتني C # و Go traces عدة مرات في ذلك.

إذن أنت لا تستمع. ارجع واقرأ الجزء الخاص بالعقود الضمنية.

قرأته ، لم يتغير شيء. في تجربتي ليست مشكلة. هذا هو الغرض من التوثيق باللغتين ( net.ParseIP على سبيل المثال). إذا نسيت التحقق مما إذا كانت القيمة صفرية / خالية أم لا ، فستواجه نفس المشكلة في كلتا اللغتين. في معظم الحالات ، سيعيد Go خطأ وسيقوم C # بطرح استثناء لذلك لا داعي للقلق بشأن الصفر. لا تقوم واجهة برمجة التطبيقات الجيدة (API) بإرجاعك فارغًا فقط دون طرح استثناء أو أي شيء لتخبرنا بما هو خطأ. في حالات أخرى ، عليك التحقق من ذلك بشكل صريح. أكثر أنواع الأخطاء شيوعًا مع القيمة الفارغة في تجربتي هي عندما يكون لديك مخازن بروتوكول حيث يكون كل حقل مؤشرًا / كائنًا أو يكون لديك منطق داخلي حيث يمكن أن تكون حقول الفئة / الهيكل معدومة اعتمادًا على الحالة الداخلية وتنسى التحقق منها من قبل التمكن من. هذا هو النمط الأكثر شيوعًا بالنسبة لي ولا شيء في Go يخفف بشكل كبير من هذه المشكلة. يمكنني تسمية شيئين يساعدان قليلاً - القيم الفارغة المفيدة وأنواع القيم. لكن الأمر يتعلق بسهولة البرمجة لأنك لست مطالبًا ببناء كل متغير قبل الاستخدام.

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

هذه مشكلة ، لم أقل أبدًا بخلاف ذلك ، لكن الناس هنا يركزون بشدة على استثناءات Java / C # / C ++ لدرجة أنهم يتجاهلون أي شيء يشبههم قليلاً. لماذا تطلب Swift منك تحديد الوظائف بـ throws حتى تتمكن من رؤية ما يجب أن تتوقعه بالضبط من إحدى الوظائف وأين يمكن أن ينقطع تدفق التحكم وفي Rust الذي تستخدمه؟ لنشر خطأ بشكل صريح باستخدام طرق مساعدة مختلفة لمنحه مزيدًا من السياق. كلاهما يستخدم نفس مفهوم الأخطاء كقيم ولكن يلفها في السكر النحوي لتقليل الصيغة المعيارية.

وهل تعد آثار Go الضخمة مع مئات من goroutines أكثر فائدة إلى حد ما؟

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

في تجربتي ليست مشكلة.

حسنًا ، تختلف تجربتي ، وأعتقد أن تجارب معظم الناس تختلف ، وكدليل على ذلك أذكر حقيقة أن Java 8 أضاف أنواعًا اختيارية.

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

https://elixirforum.com/t/discussing-go-split-thread/13006/2

سنتان للتعامل مع الخطأ (آسف إذا تم ذكر هذه الفكرة أعلاه).

نريد إعادة طرح الأخطاء في معظم الحالات. هذا يؤدي إلى مثل هذه المقتطفات:

a, err := fn()
if err != nil {
    return err
}
use(a)
return nil

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

a := fn()
use(a)

// or just

use(fn())

سيوفر المترجم err إلى متغير ضمني (غير مرئي) ، تحقق من nil (إذا أخطأ == لا شيء) أو أعده (إذا أخطأ! = لا شيء) وأعد لا شيء في النهاية من الوظيفة إذا لم تحدث أخطاء أثناء تنفيذ الوظيفة كالمعتاد ولكن تلقائيًا وضمنيًا.

إذا كان يجب التعامل مع err فيجب تخصيصه لمتغير صريح واستخدامه:

a, err := fn()
if err != nil {
    doSomething(err)
} else {
    use(a)
}
return nil

يمكن قمع الخطأ بهذه الطريقة:

a, _ := fn()
use(a)

في حالات نادرة (رائعة) مع ظهور أكثر من خطأ ، ستكون معالجة الأخطاء الصريحة إلزامية (مثل الآن):

err1, err2 := fn2()
if err1 != nil || err2 != nil {
    return err1, err2
}
return nil, nil

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

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

إذا نظرت إلى Swift ، على سبيل المثال ، فلن يتم تجميع هذا الرمز

func a() throws {}
func b() throws {
  a()
}

يمكن أن يتسبب a حدوث خطأ ، لذا عليك كتابة try a() حتى لنشر خطأ. إذا قمت بإزالة throws من b فلن يتم تجميعها حتى مع try a() . يجب عليك معالجة الخطأ داخل b . هذه طريقة أفضل بكثير للتعامل مع الأخطاء التي تحل مشكلة التحكم غير الواضح في تدفق الاستثناءات وإسهاب أخطاء Objective-C. هذا الأخير يشبه إلى حد كبير الأخطاء في Go وما من المفترض أن يحل محل Swift. ما لا يعجبني هو try, catch الأشياء التي يستخدمها Swift أيضًا. أفضل ترك الأخطاء كجزء من قيمة الإرجاع.

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

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

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

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

لماذا تفكر في التسويات؟

@ nick-korsakov ، الاقتراح الأصلي (هذه المشكلة) يريد إضافة المزيد من السياق للأخطاء:

من السهل بالفعل (ربما من السهل جدًا) تجاهل الخطأ (انظر # 20803). تعمل العديد من المقترحات الحالية الخاصة بمعالجة الأخطاء على تسهيل إعادة الخطأ بدون تعديل (على سبيل المثال ، # 16225 ، # 18721 ، # 21146 ، # 21155). القليل يسهل إرجاع الخطأ بمعلومات إضافية.

انظر أيضا هذا التعليق .

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

دراجة أخرى:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    try err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    try errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    try errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

يعني try "العودة إن لم تكن القيمة الفارغة". في هذا أفترض أن errors.Errorf سيعيد nil عندما يكون الخطأ لا شيء. أعتقد أن هذا يتعلق بقدر المدخرات التي يمكن أن نتوقعها مع التمسك بهدف التغليف السهل.

تخزن أنواع الماسح الضوئي في المكتبة القياسية حالة الخطأ داخل هيكل يمكن لأساليبها التحقق بشكل مسؤول من وجود خطأ قبل المتابعة.

type Scanner struct{
    err error
}
func (s *Scanner) Scan() bool{
   if s.err != nil{
       return false
   }
   // scanning logic
}
func (s *Scanner) Err() error{ return s.err }

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

كما أنه لا يتطلب أي تغييرات بناءة إبداعية وغريبة أو عمليات نقل تحكم غير متوقعة في اللغة.

يجب أن أقترح أيضًا شيئًا مثل try / catch ، حيث يتم تحديد err داخل try {} ، وإذا تم تعيين err إلى قيمة non-nil - فواصل التدفق من try {} إلى كتل معالج الخطأ (إن وجدت).

لا توجد استثناءات داخليًا ، ولكن يجب أن يكون الأمر برمته أقرب
لبناء الجملة الذي يقوم بعمل شيكات if err != nil break بعد كل سطر يمكن تعيين خطأ فيه.
على سبيل المثال:

...
try(err) {
   err = doSomethig()
   err, value := doSomethingElse()
   doSomethingObliviousToErr()
   err = thirdErrorProneThing()
} 
catch(err SomeErrorType) {
   handleSomeSpecificErr(err)
}
catch(err Error) {
  panic(err)
}

أعلم أنه يشبه C ++ ، ولكنه معروف أيضًا وأنظف من الدليل if err != nil {...} بعد كل سطر.

@كما

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

تضمين التغريدة

إذا أردنا معالجة خط واحد لخطأ بسيط ، فيمكننا تغيير الصيغة للسماح لبيان الإرجاع بأن يكون بداية لكتلة سطر واحد.
سيسمح للناس بكتابة:

func makeFile(url string) (size int, err error){
    rsp, err := http.Get(url)
    if err != nil return err
    defer rsp.Body.Close()

    var data dataStruct
    dec := json.NewDecoder(rsp.Body)
    err := dec.Decode(&data)
    if err != nil return errors.Errorf("could not decode %s: %v", url, err)

    f, err := os.Create(data.Path)
    if err != nil return errors.Errorf("could not open file %s: %v", data.Path, err)
    defer f.Close()

    return f.Write([]byte(data.Rows))
}

أعتقد أنه يجب تغيير المواصفات إلى شيء مثل (قد يكون هذا ساذجًا تمامًا :))

Block = "{" StatementList "}" | "return" Expression .

لا أعتقد أن عودة الغلاف الخاصة أفضل حقًا من مجرد تغيير gofmt لتبسيط الأمر إذا كان الخطأ يتحقق من سطر واحد بدلاً من ثلاثة.

urandom

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

نهج الماسح هو أحد أسوأ الأشياء التي قرأتها في سياق شعار "الأخطاء قيم" بالكامل:

  1. إنه عديم الفائدة في كل حالة استخدام تتطلب الكثير من الأخطاء في التعامل مع لوحة الغلاية. لن تستفيد منه الوظائف التي تستدعي حزمًا خارجية متعددة.
  2. إنه مفهوم معقد وغير مألوف. لن يؤدي تقديمه إلا إلى إرباك القراء المستقبليين وجعل شفرتك أكثر تعقيدًا مما يجب أن تكون عليه فقط حتى تتمكن من التغلب على قصور تصميم اللغة.
  3. إنه يخفي المنطق ويحاول أن يكون مشابهًا للاستثناءات من خلال أخذ الأسوأ منه (تدفق التحكم المعقد) دون الاستفادة من أي فوائد.
  4. في بعض الحالات سوف تهدر موارد الحساب. يجب أن تضيع كل مكالمة الوقت في التحقق من الخطأ غير المجدي الذي حدث منذ زمن طويل.
  5. إنه يخفي المكان الدقيق الذي حدث فيه الخطأ. تخيل حالة تقوم فيها بتحليل أو إجراء تسلسل لبعض تنسيقات الملفات. سيكون لديك سلسلة من مكالمات القراءة / الكتابة. تخيل أن الأول فشل. كيف تعرف أين حدث الخطأ بالضبط؟ ما هو المجال الذي تم تحليله أم تسلسله؟ "خطأ IO" ، "timeout" - ستكون هذه الأخطاء عديمة الفائدة في هذه الحالة. يمكنك توفير سياق لكل قراءة / كتابة (اسم الحقل ، على سبيل المثال). لكن في هذه المرحلة ، من الأفضل التخلي عن النهج بأكمله لأنه يعمل ضدك.

في بعض الحالات سوف تهدر موارد الحساب.

المعايير؟ ما هو "حساب المورد" بالضبط؟

إنه يخفي المكان الدقيق الذي حدث فيه الخطأ.

لا ، لا ، لأنه لا يتم الكتابة فوق الأخطاء غير الصفرية

لن تستفيد منه الوظائف التي تستدعي حزمًا خارجية متعددة.
إنه مفهوم معقد وغير مألوف
نهج الماسح هو أحد أسوأ الأشياء التي قرأتها في سياق هذا كله "الأخطاء هي القيم"

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

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

عرض

قلل نموذج الخطأ المعياري عن طريق تمكين استخدام الرمز المميز _! كسكر نحوي لإحداث حالة من الذعر عند تعيين قيمة غير معدومة error

val, err := something.MayError()
if err != nil {
    panic(err)
}

يمكن أن تصبح

val, _! := something.MayError()

و

if err := something.MayError(); err != nil {
    panic(err)
}

يمكن أن تصبح

_! = something.MayError()

بالطبع ، الرمز الخاص مطروح للنقاش. لقد نظرت أيضًا في _^ ، _* ، @ ، وغيرها. لقد اخترت _! كاقتراح واقعي لأنني اعتقدت أنه سيكون الأكثر شيوعًا في لمحة.

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

التبرير

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

أدرك أن فلسفة go's هي تجنب الذعر قدر الإمكان. إنها ليست أداة لنشر الخطأ عبر حدود API. ومع ذلك ، فهي ميزة للغة ولها حالات استخدام مشروعة ، مثل تلك المذكورة أعلاه. الذعر طريقة رائعة لتبسيط انتشار الخطأ في الكود الخاص ، وتبسيط بناء الجملة سيقطع شوطًا طويلاً لجعل الكود أنظف وأكثر وضوحًا. أشعر أنه من الأسهل التعرف على _! (أو @ ، أو `_ ^ ، إلخ ...) في لمحة من التعرف على نموذج" if-error-panic ". يمكن للرمز المميز أن يقلل بشكل كبير من مقدار الكود الذي يجب كتابته / قراءته للتعبير / الفهم:

  1. يمكن أن يكون هناك خطأ
  2. إذا كان هناك خطأ ، فنحن لا نتوقعه
  3. إذا كان هناك خطأ ، فمن المحتمل أنه يتم التعامل معه في السلسلة

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

بالإضافة إلى تبسيط حالات الاستخدام المقبولة للذعر ، فإن هذا أيضًا يجعل النماذج الأولية السريعة أسهل. إذا كانت لدي فكرة أريد تدوينها في الكود ، وأريد فقط أن تؤدي الأخطاء إلى تعطل البرنامج أثناء اللعب ، يمكنني الاستفادة من إضافة بناء الجملة هذه بدلاً من نموذج "if-error-panic". إذا كان بإمكاني التعبير عن نفسي في سطور أقل في المراحل الأولى من التطوير ، فهذا يسمح لي بتحويل أفكاري إلى رمز بشكل أسرع. بمجرد أن تكون لدي فكرة كاملة في الكود ، أعود وأعيد تشكيل الكود الخاص بي لإرجاع الأخطاء في الحدود المناسبة. لن أترك ذعرًا مجانيًا في كود الإنتاج ، لكن يمكن أن تكون أداة تطوير قوية.

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

carlmjohnson يجب أن يكون أحد أمرين صحيحًا:

  1. الذعر هو جزء من اللغة مع حالات الاستخدام المشروعة ، أو
  2. الذعر ليس له حالات استخدام مشروعة ، وبالتالي يجب إزالته من اللغة

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

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

الذعر ينقل شيئًا محددًا:

حدث خطأ ما هنا أن هنا لم يكن مستعدا لمقبض

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

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

var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

أكثر قابلية للقراءة من

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

لا يوجد فرق جوهري بين الذعر في Go والاستثناء في Java أو Python وما إلى ذلك بصرف النظر عن التركيب وعدم وجود تسلسل هرمي للكائن (وهو أمر منطقي لأن Go ليس لديه وراثة). كيف تعمل وكيف يتم استخدامها هي نفسها.

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

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

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

يستخدم الأشخاص أحيانًا الذعر كوسيلة لتقصير دائرة المكدس ، ولكن هذا أمر مستهجن عمومًا لأسباب تتعلق بقابلية القراءة والأداء

بادئ ذي بدء ، فإن مشكلة سهولة القراءة هي شيء يعالجه هذا التغيير في بناء الجملة مباشرة:

// clearly, linearly shows that these steps must occur in order,
// and any errors returned cause a panic, because this piece of
// code isn't responsible for reporting or handling possible failures:
// - IO Error: either network or disk read/write failed
// - External service error: some unexpected response from the external service
// - etc...
// It's not this code's responsibility to be aware of or handle those scenarios.
// That's perhaps the parent process's job.
var1, _! := trySomeTask1()
var2, _! := trySomeTask2(var1)
var3, _! := trySomeTask3(var2)
var4, _! := trySomeTask4(var3)

ضد

var1, err := trySomeTask1()
if err != nil {
    panic(err)
}
var2, err := trySomeTask2(var1)
if err != nil {
    panic(err)
}
var3, err := trySomeTask3(var2)
if err != nil {
    panic(err)
}
var4, err := trySomeTask4(var3)
if err != nil {
    panic(err)
}

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

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

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

  1. ما هي الإساءات التي تتخيل أنها ستنشأ من امتداد نحوي مثل هذا؟
  2. ما الذي يقدمه var1, err := trySomeTask1(); if err != nil { panic(err) } والذي لا يقدمه var1, _! := trySomeTask1() ؟ لماذا ا؟

يبدو لي أن جوهر حجتك هو أن "الذعر سيء ويجب ألا نستخدمه".
لا يمكنني تفريغ ومناقشة الأسباب الكامنة وراء ذلك إذا لم يتم مشاركتها.

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

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

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

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

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

على سبيل المثال ، لنفترض أن لديك http.HandlerFunc اقرأ ملفًا من القرص كاستجابة - سيعمل هذا دائمًا تقريبًا ، وإذا فشل ، فمن المحتمل إما أن البرنامج غير مكتوب بشكل صحيح (خطأ مبرمج) أو أن هناك مشكلة في نظام الملفات خارج نطاق مسؤولية البرنامج. بمجرد ذعر http.HandlerFunc ، فإنه يخرج وسيصيب بعض المعالج الأساسي هذا الذعر ويكتب 500 إلى العميل. إذا كنت أرغب في أي وقت في المستقبل في التعامل مع هذا الخطأ بشكل مختلف ، فيمكنني استبدال _! بـ err والقيام بكل ما أريد بقيمة الخطأ. الشيء هو ، طوال مدة البرنامج ، من المحتمل ألا أحتاج إلى القيام بذلك. إذا واجهت مشكلات من هذا القبيل ، فإن المعالج ليس جزءًا من الكود مسؤولاً عن معالجة هذا الخطأ.

يمكنني ، وعادة ما أفعل ، كتابة if err != nil { panic(err) } أو if err != nil { return ..., err } في معالجاتي لأشياء مثل فشل الإدخال / الإخراج ، وفشل الشبكة ، وما إلى ذلك. عندما أحتاج إلى التحقق من خطأ ، لا يزال بإمكاني القيام بذلك. في معظم الأوقات ، أكتب فقط if err != nil { panic(err) } .

أو ، في مثال آخر ، إذا كنت أبحث بشكل متكرر عن ثلاثي (على سبيل المثال في تطبيق موجه http) ، فسأعلن عن وظيفة func (root *Node) Find(path string) (found Value, err error) . ستؤجل هذه الوظيفة وظيفة لاستعادة أي حالات ذعر ناتجة عن نزول الشجرة. ماذا لو كان البرنامج يقوم بإنشاء محاولات غير صحيحة؟ ماذا لو فشل بعض IO لأن البرنامج لا يعمل كمستخدم لديه الأذونات الصحيحة؟ هذه المشكلات ليست مشكلة خوارزمية البحث الثلاثي - إلا إذا قمت بذلك صراحةً في وقت لاحق - لكنها أخطاء محتملة قد أواجهها. تؤدي إعادتها إلى أعلى المكدس إلى الكثير من الإسهاب الإضافي ، بما في ذلك الاحتفاظ بما سيكون من الناحية المثالية عدة قيم خطأ صفرية في المكدس. بدلاً من ذلك ، يمكنني اختيار إثارة خطأ في وظيفة واجهة برمجة التطبيقات العامة وإعادتها إلى المستخدم. في الوقت الحالي ، لا يزال هذا يتطلب الإسهاب الإضافي ، لكن لا داعي لذلك.

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

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

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

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

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

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

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

ولكن في كثير من الحالات يكون هذا الاختلاف ضئيلًا بالنسبة للعملية التي يتم إجراؤها

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

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

لا شكرا. اختناقات الأداء الخيالية هي اختناقات أداء مهملة هندسيًا.

لا شكرا. اختناقات الأداء الخيالية هي اختناقات أداء مهملة هندسيًا.

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

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

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

https://golang.org/conduct

أرغب في زيارة جزء "إرجاع الخطأ / مع سياق إضافي" مرة أخرى ، حيث أفترض أن تجاهل الخطأ مغطى بالفعل بواسطة _ الموجود بالفعل.

أقترح كلمة رئيسية من كلمتين يمكن أن تتبعها سلسلة (اختياريًا). السبب في أنها كلمة رئيسية من كلمتين ذو شقين. أولاً ، على عكس العامل المشفر بطبيعته ، من الأسهل فهم ما يفعله دون معرفة مسبقة أكثر من اللازم. لقد اخترت "أو فقاعة" ، لأنني آمل أن تشير الكلمة or مع عدم وجود خطأ معين للمستخدم إلى أنه يتم التعامل مع الخطأ هنا ، إذا لم يكن لا شيء. سيقوم بعض المستخدمين بالفعل بربط or بالتعامل مع قيمة زائفة من لغات أخرى (perl ، python) ، وقراءة data := Foo() or ... قد تخبرهم دون وعي أن data غير قابل للاستخدام إذا كان or جزء من البيان. ثانيًا ، قد تشير الكلمة الأساسية bubble بينما تكون قصيرة نسبيًا ، للمستخدم إلى أن شيئًا ما يرتفع (المكدس). قد تكون الكلمة up مناسبة أيضًا ، على الرغم من أنني لست متأكدًا مما إذا كانت الكلمة or up كلها مفهومة بشكل كافٍ. أخيرًا ، الأمر برمته عبارة عن كلمة رئيسية ، أولاً وقبل كل شيء لأنها أكثر قابلية للقراءة ، وثانيًا لأن هذا السلوك لا يمكن كتابته بواسطة دالة نفسها (قد تكون قادرًا على استدعاء الذعر للهروب من الوظيفة التي أنت فيها ، ولكن بعد ذلك يمكنك ' ر توقف نفسك ، سيتعين على شخص آخر أن يتعافى).

ما يلي مخصص فقط لنشر الخطأ ، وبالتالي لا يجوز استخدامه إلا في الدالات التي تُرجع خطأً ، والقيم الصفرية لأي وسيطات إرجاع أخرى:

لإرجاع خطأ دون تعديله بأي شكل من الأشكال:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble

    return data;
}

لإرجاع خطأ برسالة إضافية:

func Worker(path string) ([]byte, error) {
    data := ioutil.ReadFile(path) or bubble fmt.Sprintf("reading file %s", path)

    modified := modifyData(data) or bubble "modifying the data"

    return data;
}

وأخيرًا ، قدم آلية محول عالمية لتعديل الخطأ المخصص:

// Default Bubble Processor
errors.BubbleProcessor(func(msg string, err error) error {
    return fmt.Errorf("%s: %v", msg, err)
})

// Some program might register the following:
errors.BubbleProcessor(func(msg string, err error) error {
    return errors.WithMessage(err, msg)
})

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

مثير للاهتمام. إن وجود معالج فقاعات عالمي يمنح الأشخاص الذين يريدون تتبع المكدس مكانًا لوضع استدعاء للتتبع ، وهي فائدة جيدة لهذه الطريقة. OTOH ، إذا كان يحتوي على التوقيع func(string, error) error ، فهذا يعني أن الفقاعة يجب أن تتم مع نوع الخطأ المضمن وليس أي نوع آخر ، مثل النوع الملموس الذي يستخدم error .

أيضًا ، وجود or bubble يوحي بإمكانية or die أو or panic . لست متأكدًا مما إذا كانت هذه ميزة أم خطأ.

على عكس المشغل الذي يكون خفيًا بطبيعته ، من الأسهل فهم ما يفعله دون معرفة مسبقة كثيرة

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

لست معجبًا كثيرًا بمحول عالمي. إذا كانت الحزمة الخاصة بي تعيّن معالجًا مخصصًا ، وتضع حزمتك أيضًا معالجًا مخصصًا ، فمن سيفوز؟

@ object88
أعتقد أنه مشابه للمسجل الافتراضي. تقوم بتعيين الإخراج مرة واحدة فقط (في برنامجك) ، وهذا يؤثر على جميع الحزم التي تستخدمها.

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

يرجى إعادة المحاولة أخيرًا ولن نحتاج بعد الآن إلى المعارك. يجعل الجميع سعداء. لا حرج في استعارة الميزات والنحو من لغات البرمجة الأخرى. قامت Java بذلك وفعلت C # ذلك أيضًا وكلاهما من لغات البرمجة الناجحة حقًا. مجتمع GO (أو المؤلفين) ، يرجى الانفتاح على التغييرات عند الحاجة.

KamyarM ، أنا

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

على الرغم من أنه يمكنك تقديم مثال قصير ، ربما هناك شيء مفقود.

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

ربما لا يرتبط هذا بهذه الحالة ولكني كنت أبحث في يوم آخر عن Go ما يعادل عامل التشغيل الثلاثي لـ C ++ ووجدت هذا النهج البديل:

v: = map [bool] int {true: first_expression، false: second_expression} [condition]
بدلا من البساطة
ت = الشرط؟ التعبير الأول: التعبير_الثاني ؛

أي من الشكلين تفضلين يا رفاق؟ رمز غير قابل للقراءة أعلاه (Go My Way) مع الكثير من مشكلات الأداء أو الصيغة البسيطة الثانية في C ++ (الطريق السريع)؟ أنا أفضل الرجال الطريق السريع. انا لا اعرف عنك.

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

أنا في حاجة إليها،

مجتمع GO ليس منفتحًا على التغييرات وأنا أشعر بذلك وأنا لا أحب ذلك حقًا.

أعتقد أن هذا يسيء وصف الموقف الكامن وراء ما تواجهه. نعم ، ينتج المجتمع الكثير من الدعم عندما يقترح أحدهم المحاولة / الصيد أو؟:. لكن السبب ليس أننا نقاوم الأفكار الجديدة. لدينا جميعًا تقريبًا خبرة في استخدام اللغات مع هذه الميزات. نحن على دراية بها تمامًا ، وقد استخدمها شخص منا بشكل يومي لسنوات. تستند مقاومتنا إلى حقيقة أن هذه _أفكار قديمة_ وليست جديدة. لقد تبنينا بالفعل تغييرًا: تغيير بعيدًا عن المحاولة / الالتقاط وتغيير بعيدًا عن الاستخدام؟:. ما نقاومه هو تغيير _back_ لاستخدام هذه الأشياء التي استخدمناها بالفعل ولم نحبها.

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

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

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

KamyarM اللغة

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

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

    var v int
    if condition {
        v = first_expression
    } else {
        v = second_expression
    }

لا يعني ذلك أننا لا نريد استعارة بناء الجملة ، بل علينا التفكير في كيفية ملاءمتها لبقية اللغة وبقية الكود الموجود بالفعل اليوم.

KamyarM أستخدم كلاً من Go و Java ، وأنا بالتأكيد لا أريد الانتقال لنسخ معالجة الاستثناءات من Java. إذا كنت تريد Java ، فاستخدم Java. ويرجى أخذ مناقشة العوامل الثلاثية إلى قضية مناسبة ، على سبيل المثال # 23248.

lpar لذا إذا كنت أعمل في شركة ولسبب غير معروف اختاروا GoLang كلغة برمجتهم ، فأنا ببساطة بحاجة إلى ترك وظيفتي والتقدم بطلب للحصول على Java !؟ هيا يا صاح!

bcmills يمكنك حساب الكود الذي اقترحته هناك. أعتقد أن هذا هو 6 أسطر من التعليمات البرمجية بدلاً من سطر واحد ، وربما تحصل على نقطتين من نقاط التعقيد السيكلوماتيكي للشفرة لذلك (أنتم يا رفاق تستخدمون Linter. أليس كذلك؟).

carlmjohnson و bcmills أي بناء جملة قديم وناضج لا يعني أن ذلك سيء. في الواقع ، أعتقد أن صيغة if else أقدم من صيغة المشغل الثلاثي.

جيد أنك أحضرت هذا الشيء المصطلح GO. أعتقد أن هذه مجرد واحدة من قضايا هذه اللغة. عندما يكون هناك طلب لإجراء تغييرات ، يقول أحدهم أوه لا ، فهذا مخالف للغة Go idiom. أرى أنه مجرد ذريعة لمقاومة التغييرات ومنع أي أفكار جديدة.

KamyarM يرجى أن تكون مهذبا. إذا كنت ترغب في قراءة المزيد عن بعض الأفكار وراء إبقاء اللغة صغيرة ، فإنني أوصي بـ https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html.

أيضًا ، تعليق عام ، لا علاقة له بالمناقشة الأخيرة حول try / catch.

كان هناك العديد من المقترحات في هذا الموضوع. بالحديث عن نفسي ، ما زلت لا أشعر أن لدي فهمًا قويًا للمشكلة (المشكلات) التي يتعين حلها. أود أن أسمع المزيد عنهم.

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

josharian كنت أتحدث بصراحة هناك. كنت أرغب في عرض المشكلات الدقيقة في اللغة أو المجتمع. اعتبر ذلك نقدًا أكثر. GoLang منفتحة على النقد ، هل هذا صحيح؟

KamyarM إذا كنت تعمل لدى شركة اختارت Rust للغة البرمجة الخاصة بها ، فهل ستذهب إلى Rust Github وتبدأ في طلب إدارة الذاكرة المجمعة بالقمامة ومؤشرات نمط C ++ حتى لا تضطر إلى التعامل مع مدقق الاستعارة؟

السبب وراء عدم رغبة مبرمجي Go في الحصول على استثناءات على غرار Java لا علاقة له بنقص الإلمام بها. لقد واجهت استثناءات لأول مرة في عام 1988 عبر Lisp ، وأنا متأكد من أن هناك أشخاصًا آخرين في هذا الموضوع قد واجهوها في وقت سابق - تعود الفكرة إلى أوائل السبعينيات.

وينطبق الشيء نفسه على التعبيرات الثلاثية. اقرأ عن تاريخ Go - قام كين طومسون ، أحد مطوري Go ، بتنفيذ المشغل الثلاثي بلغة B (سلف C) في Bell Labs في عام 1969. أعتقد أنه من الآمن القول إنه كان على دراية بفوائدها ومخاطرها عند التفكير سواء لتضمينها في Go.

Go مفتوح للنقد ولكننا نطلب أن تكون المناقشة في منتديات Go مهذبة. الصراحة لا يعني أن تكون غير مهذب. راجع قسم "قيم غوفر" من https://golang.org/conduct. شكرا.

lpar نعم ، إذا كان لدى Rust مثل هذا المنتدى سأفعل ذلك ؛-) بجدية سأفعل ذلك. لأنني أريد أن يُسمع صوتي.

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

josharian شكرا لك على المقال سوف ألقي نظرة على ذلك.

حسنًا ، لقد نظرت إلى تعليقاتي مدعومة لمعرفة ما إذا كان هناك أي شيء سيء. الشيء الوحيد الذي قد أكون قد أهنته (ما زلت أسمي هذا النقد راجع للشغل) هو لغة برمجة GoLang التعابير! ههههه!

للرجوع إلى موضوعنا ، إذا سمعت صوتي ، فيرجى Go Authors التفكير في إعادة قوالب Try catch. اترك الأمر للمبرمج ليقرر استخدامه في المكان المناسب أم لا (لديك بالفعل شيء مشابه ، أعني تأجيل الذعر للتعافي ، فلماذا لا تجرب Catch الأكثر دراية للمبرمجين؟).
اقترحت حلاً بديلاً لمعالجة Go Error الحالية للتوافق مع الإصدارات السابقة. أنا لا أقول أن هذا هو الخيار الأفضل ولكني أعتقد أنه قابل للتطبيق.

سوف أتقاعد من مناقشة المزيد حول هذا الموضوع.

شكرا لكم على إتاحة الفرصة.

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

مرة أخرى: من فضلك كن مهذبا. التزم بالحجج التقنية. تجنب الحجج الإعلانية التي تهاجم الناس بدلاً من الأفكار. إذا كنت حقيقيًا ولا تفهم ما أعنيه ، فأنا على استعداد لمناقشته في وضع عدم الاتصال ؛ راسلني. شكرا.

سأضع 2c في ، وآمل ألا أكرر شيئًا ما في التعليقات الـ N الأخرى (أو أتدخل في مناقشة اقتراح urandom).

تعجبني الفكرة الأصلية التي تم نشرها ، ولكن مع تعديلين أساسيين:

  • تربيع الدرجات النحوي: أعتقد اعتقادًا راسخًا أن أي شيء يحتوي على تحكم ضمني في التدفق يجب أن يكون عاملاً بمفرده ، بدلاً من تحميل زائد لمشغل موجود. سأرمي ?! هناك ، لكنني سعيد بكل ما لا يمكن الخلط بينه وبين عامل حالي في Go.

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

func returnErrorf(s string, args ...interface{}) func(error) error {
  return func(err error) error {
    return errors.New(fmt.Sprintf(s, args...) + ": " + err.Error())
  }
}

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) ?! returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() ?! returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() ?! func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

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

من السهل أيضًا "زيادة التحميل" على هذا النمط للعمل في حالات أكثر إثارة للاهتمام. لدي ثلاثة أمثلة في الاعتبار.

أولاً ، يمكن أن يكون return مشروطًا إذا كان RHS هو func(error) (error, bool) ، مثل ذلك (إذا سمحنا بذلك ، أعتقد أنه يجب علينا استخدام عامل تشغيل مميز من العوائد غير المشروطة. استخدم ?? ، لكن عبارة "لا أهتم طالما أنها مميزة" لا تزال سارية):

func maybeReturnError(err error) (error, bool) {
  if err == io.EOF {
    return nil, false
  }
  return err, true
}

func id(err error) error { return err }

func ignoreError(err error) (error, bool) { return nil, false }

func foo(n int) error {
  // Does nothing
  id(io.EOF) ?? ignoreError
  // Still does nothing
  id(io.EOF) ?? maybeReturnError
  // Returns the given error
  id(errors.New("oh no")) ?? maybeReturnError
  return nil
}

بدلاً من ذلك ، يمكننا قبول وظائف RHS التي لها أنواع إرجاع تطابق تلك الخاصة بالوظيفة الخارجية ، مثل:

func foo(r io.Reader) ([]int, error) {
  returnError := func(err error) ([]int, error) { return []int{0}, err }
  // returns `[]int{0}, err` on a Read failure
  n := r.Read(make([]byte, 4)) ?! returnError
  return []int{n}, nil
}

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

func returnOpFailed(name string) func(bool) error {
  return func(_ bool) error {
    return errors.New(name + " failed")
  }
}

func returnErrOpFailed(name string) func(error) error {
  return func(err error) error {
    return errors.New(name + " failed: " + err.Error())
  }
}

func foo(c chan int, readInt func() (int, error), d map[int]string) (string, error) {
  n := <-c ?! returnOpFailed("receiving from channel")
  m := readInt() ?! returnErrOpFailed("reading an int")
  result := d[n + m] ?! returnOpFailed("looking up the number")
  return result, nil
}

... وهو ما أجده مفيدًا شخصيًا عندما يتعين علي القيام بشيء فظيع ، مثل فك تشفير map[string]interface{} يدويًا.

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

أرغب في زيارة جزء "إرجاع الخطأ / مع سياق إضافي" مرة أخرى ، لأنني أفترض أن تجاهل الخطأ قد تم تغطيته بالفعل بواسطة _ الموجود بالفعل.

أقترح كلمة رئيسية من كلمتين يمكن أن تتبعها سلسلة (اختياريًا).

@ عشوائي ، الجزء الأول من BubbleProcessor للمراجعة الثانية. المخاوف التي أثارتها @ object88 هي IMO صالحة ؛ لقد رأيت مؤخرًا نصائح مثل "يجب ألا تكتب فوق العميل / النقل الافتراضي لـ http " ، فقد تصبح هذه إحدى تلك النصائح.

كان هناك العديد من المقترحات في هذا الموضوع. بالحديث عن نفسي ، ما زلت لا أشعر أن لدي فهمًا قويًا للمشكلة (المشكلات) التي يتعين حلها. أود أن أسمع المزيد عنهم.

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

يمكن أن تكون أنت @ josharian إذا قام ianlancetaylor بتعيينك ؟ : أحمر الخدود: لا أعرف كيف يتم التخطيط / مناقشة القضايا الأخرى ولكن ربما يتم استخدام هذه المناقشة فقط كـ "صندوق اقتراحات"؟

تضمين التغريدة

bcmills يمكنك حساب الكود الذي اقترحته هناك. أعتقد أن هذا هو 6 أسطر من التعليمات البرمجية بدلاً من سطر واحد ، وربما تحصل على نقطتين من نقاط التعقيد السيكلوماتيكي للشفرة لذلك (أنتم يا رفاق تستخدمون Linter. أليس كذلك؟).

يجعل إخفاء التعقيد السيكلومي من الصعب رؤيته ولكنه لا يزيله (تذكر strlen ؟). تمامًا مثل جعل معالجة الأخطاء "مختصرة" يجعل تجاهل معالجة الدلالات الخطأ أسهل - ولكن من الصعب رؤيتها.

يجب أن تكون أي عبارات أو تعبيرات في المصدر تعيد توجيه التحكم في التدفق واضحة ومختصرة ، ولكن إذا كان قرارًا بين واضح أو مقتضب ، فيجب تفضيل ما هو واضح في هذه الحالة.

جيد أنك أحضرت هذا الشيء المصطلح GO. أعتقد أن هذه مجرد واحدة من قضايا هذه اللغة. عندما يكون هناك طلب لإجراء تغييرات ، يقول أحدهم أوه لا ، فهذا مخالف للغة Go idiom. أرى أنه مجرد ذريعة لمقاومة التغييرات ومنع أي أفكار جديدة.

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

ربما تعتقد أن فكرتك أفضل من غيرها. هذا هو المكان الذي تأتي فيه المناقشة. بدلاً من تحطيم المحادثة للحديث عن كيفية تعطل النظام بأكمله بسبب التعبيرات الاصطلاحية ، تعامل مع الانتقادات بشكل مباشر ، نقطة بنقطة ، أو ابحث عن حل وسط بينك وبين أقرانك.

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

يعجبني أيضًا اقتراح gburgessiv ، على الرغم من أنني لست معجبًا كبيرًا بالمشغل المشفر نفسه (ربما على الأقل اختيار ? مثل Rust ، على الرغم من أنني ما زلت أعتقد أن هذا مشفر). هل سيبدو هذا أكثر قابلية للقراءة:

func foo(r io.ReadCloser, callAfterClosing func() error, bs []byte) ([]byte, error) {
  // If r.Read fails, returns `nil, errors.New("reading from r: " + err.Error())`
  n := r.Read(bs) or returnErrorf("reading from r")
  bs = bs[:n]
  // If r.Close() fails, returns `nil, errors.New("closing r after reading [[bs's contents]]: " + err.Error())`
  r.Close() or returnErrorf("closing r after reading %q", string(bs))
  // Not that I advocate this inline-func approach, but...
  callAfterClosing() or func(err error) error { return errors.New("oh no!") }
  return bs, nil
}

ونأمل أن يتضمن اقتراحه أيضًا تطبيقًا افتراضيًا لوظيفة مشابهة لعودته Errorf في مكان ما في حزمة errors . ربما errors.Returnf() .

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

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

المشكلة التي نحاول معالجتها هي الفوضى المرئية الناتجة عن معالجة أخطاء Go. هذا مثال جيد ( المصدر ):

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {
    if err := createFolderIfNotExist(to); err != nil {
        return nil, err
    }
    if err := clearFolder(to); err != nil {
        return nil, err
    }
    if err := cloneRepo(to, from); err != nil {
        return nil, err
    }
    dirs, err := getContentFolders(to)
    if err != nil {
        return nil, err
    }
    return dirs, nil
}

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

المقترحات التي تحاول تبسيط كود مثل هذا ينقسم إلى مجموعات قليلة.

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

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

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

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

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

أخيرًا وليس آخرًا ، تدرك mpvl أن معالجة الأخطاء يمكن أن تكون صعبة للغاية في حالة الذعر. يقترح تغييرًا أكثر جذرية في معالجة الأخطاء Go لتحسين الدقة وكذلك سهولة القراءة. لديه حجة مقنعة ، لكن في النهاية أعتقد أن قضاياه لا تتطلب تغييرات جذرية ويمكن التعامل معها بالآليات الموجودة .

أعتذر لأي شخص أفكاره ليست ممثلة هنا.

لدي شعور بأن أحدهم سيسألني عن الفرق بين السكر والسحر. (أنا أسأل نفسي ذلك).

السكر هو نوع من التركيب اللغوي الذي يختصر الشفرة دون تغيير قواعد اللغة بشكل أساسي. عامل التخصيص القصير := هو sugar. وكذلك الأمر بالنسبة إلى عامل التشغيل الثلاثي C ?: .

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

الخط ضبابي بالتأكيد.

شكرا لفعل ذلك ، jba. مفيد جدا. فقط لاستخراج النقاط البارزة ، فإن المشاكل التي تم تحديدها حتى الآن هي:

الفوضى المرئية الناتجة عن معالجة أخطاء Go

و

يمكن أن يصبح التعامل مع الأخطاء أمرًا صعبًا للغاية في حالة الذعر

إذا كانت هناك أي مشاكل أخرى مختلفة اختلافًا جوهريًا (ليست حلولًا) فقدتها أنا و صوتي (لأي شخص). FWIW ، أود أن أعتبر بيئة العمل ، وفوضى الكود ، والتعقيد السيكلومي ، والتلعثم ، والنماذج المعيارية ، وما إلى ذلك كمتغيرات لمشكلة "الفوضى المرئية" (أو مجموعة المشاكل).

josharian هل تريد اعتبار مشاكل تحديد النطاق (https://github.com/golang/go/issues/21161#issuecomment-319277657) كمتغير لمشكلة "الفوضى المرئية" ، أو مشكلة منفصلة؟

يبدو أن bcmills مميزًا بالنسبة لي ، نظرًا لأنه يتعلق بمسائل التصحيح الدقيقة ، بدلاً من الجماليات / بيئة العمل (أو في معظم مشكلات الصحة التي تتضمن مجموعة التعليمات البرمجية). شكرا! هل تريد تعديل تعليقي وإضافة ملخص من سطر واحد له؟

لدي شعور بأن أحدهم سيسألني عن الفرق بين السكر والسحر. (أنا أسأل نفسي ذلك).

أستخدم هذا التعريف للسحر: بالنظر إلى القليل من شفرة المصدر ، إذا كان بإمكانك معرفة ما يفترض أن تفعله من خلال بعض المتغيرات من الخوارزمية التالية:

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

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

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

الشيء الجميل في هذا هو أنه يخرج كل الذاتية تقريبًا من السؤال.

بالعودة إلى الموضوع المطروح ، يبدو أن هناك الكثير من المقترحات التي تم تقديمها بالفعل ، واحتمالات قيام شخص آخر بحل هذا من خلال اقتراح آخر يجعل الجميع يذهبون "نعم! هذا كل شيء!" تقترب من الصفر.

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

على سبيل المثال ، لقد رأيت بعض الشكاوى حول إضافة قفزات إضافية في تدفق التحكم. لكن بالنسبة لي ، بلغة الاقتراح الأصلي للغاية ، فأنا أقدر ألا أضطر إلى إضافة || &PathError{"chdir", dir, err} ثماني مرات داخل دالة إذا كانت شائعة. (أعلم أن Go ليس حساسًا للشفرة المتكررة مثل بعض اللغات الأخرى ، ولكن مع ذلك ، فإن الكود المكرر معرض لخطر كبير جدًا لأخطاء الاختلاف.) ولكن إلى حد كبير بحكم التعريف ، إذا كانت هناك آلية لاستبعاد مثل هذا التعامل مع الأخطاء ، لا يمكن أن تتدفق الشفرة من أعلى إلى أسفل ومن اليسار إلى اليمين بدون قفزات. أيهما يعتبر بشكل عام أكثر أهمية؟ أظن أن الفحص الدقيق للمتطلبات التي يضعها الأشخاص ضمنيًا على الكود سيكشف عن متطلبات أخرى متناقضة بشكل متبادل.

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

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

لقد كنت أفكر في هذا منذ المرة الثانية التي كتبت فيها إذا أخطأت! الشرط لا نعود.

لست متأكدًا من مدى نجاح هذا من حيث التحليل / التجميع ولكن يبدو أنه من السهل بما يكفي تفسيره على أنه عبارة if حيث يكون "؟" يتم رؤيته دون كسر التوافق حيث لا يتم رؤيته ، لذلك فكرت في طرحه كخيار.

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

لذلك قد تفعل شيئًا مثل هذا:

func example1() error {
    err := doSomething()
    return err != nil ? err
    //more code
}

func example2() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, err
    //more code
}

يمكننا أيضًا القيام بأشياء مثل هذه عندما يكون لدينا بعض التعليمات البرمجية للتنظيف ، بافتراض أن handleErr أعاد خطأ:

func example3() error {
    err := doSomething()
    return err !=nil ? handleErr(err)
    //more code
}

func example4() (*Mything, error) {
    err := doSomething()
    return err != nil ? nil, handleErr(err)
    //more code
}

ربما يتبع ذلك أيضًا أنك ستكون قادرًا على تقليل هذا إلى بطانة واحدة إذا أردت:

func example5() error {
    return err := doSomething(); err !=nil ? handleErr(err)
    //more code
}

func example6() (*Mything, error) {
    return err := doSomething(); err !=nil ? nil, handleErr(err)
    //more code
}

قد يبدو مثال الجلب السابق من jba كما يلي:

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return err := createFolderIfNotExist(to); err != nil ? nil, err
    return err := clearFolder(to); err != nil ? nil, err
    return err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

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

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

func (ds *GitDataSource) Fetch(from, to string) ([]string, error) {

    return? err := createFolderIfNotExist(to); err != nil ? nil, err
    return? err := clearFolder(to); err != nil ? nil, err
    return? err := cloneRepo(to, from); err != nil ? nil, err
    dirs, err := getContentFolders(to)

    return dirs, err
}

لا يبدو أن هناك فرقًا كبيرًا بين

return err != nil ? err

و

 if err != nil { return err }

أيضًا ، في بعض الأحيان قد ترغب في القيام بشيء آخر غير العودة ، مثل استدعاء panic أو log.Fatal .

لقد كنت أتحدث عن هذا منذ أن قدمت عرضًا الأسبوع الماضي ، وتوصلت إلى استنتاج أنني أتفق مع ونفحص ما نحب حول كل ، لا يعجبني كل منها ، وما هي أولويات الحل "المناسب".

المتطلبات الأكثر شيوعًا هي أنه في نهاية اليوم ، يحتاج Go إلى أن يكون قادرًا على التعامل مع 4 حالات من معالجة الأخطاء:

  1. تجاهل الخطأ
  2. إرجاع الخطأ بدون تعديل
  3. إرجاع الخطأ مع إضافة السياق
  4. الذعر (أو قتل البرنامج)

يبدو أن المقترحات تندرج في واحدة من ثلاث فئات:

  1. العودة إلى نمط try-catch-finally لمعالجة الأخطاء.
  2. أضف بناء جملة جديدًا للتعامل مع جميع الحالات الأربع المذكورة أعلاه
  3. التأكيد على ذلك يتعامل مع بعض الحالات بشكل جيد بما فيه الكفاية ، ويقترح بناء الجملة / المبني للمساعدة في الحالات الأخرى.

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

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

لدى jba تعليق تلخيص آخر لطيف أعلاه ، لمزيد من السياق. يقول الكثير مما قلته هنا ، بكلمات مختلفة.

ianlancetaylor ، أو أي شخص آخر أكثر

لا أعتقد أنني أستطيع كتابة مجموعة رسمية شاملة من المعايير. أفضل ما يمكنني فعله هو قائمة غير كاملة بالأشياء المهمة التي لا ينبغي تجاهلها إلا إذا كانت هناك فائدة كبيرة من القيام بذلك.

  • دعم جيد لـ 1) تجاهل خطأ ؛ 2) إرجاع خطأ دون تعديل ؛ 3) تغليف خطأ بسياق إضافي.
  • بينما يجب أن يكون رمز معالجة الأخطاء واضحًا ، يجب ألا يهيمن على الوظيفة. يجب أن يكون من السهل قراءة رمز معالجة عدم وجود أخطاء.
  • يجب أن يستمر رمز Go 1 الحالي في العمل ، أو على الأقل يجب أن يكون من الممكن ترجمة Go 1 ميكانيكيًا إلى النهج الجديد بموثوقية كاملة.
  • يجب أن يشجع النهج الجديد المبرمجين على معالجة الأخطاء بشكل صحيح. من المفترض أن يكون من السهل فعل الشيء الصحيح ، مهما كان الشيء الصحيح في أي موقف.
  • يجب أن يكون أي نهج جديد أقصر و / أو أقل تكرارًا من النهج الحالي ، مع الحفاظ على الوضوح.
  • اللغة تعمل فعلاً اليوم ، وكل تغيير له تكلفة. يجب أن تكون فائدة التغيير تستحق التكلفة بوضوح. لا ينبغي أن يكون مجرد غسيل ، بل يجب أن يكون أفضل بشكل واضح.

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

لقد استخرجت هذا من مشروع حقيقي خاص بي وقمت بتنظيفه لإصداره خارجيًا. (أعتقد أن stdlib ليس هو أفضل مصدر ، لأنه يفتقد لمخاوف التسجيل ، من بين أمور أخرى ..)

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    if err != nil {
        return nil, err
    }
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    if err != nil {
        return nil, err
    }

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    if err != nil {
        return nil, err
    }

    session, err := communicationProtocol.FinalProtocol(conn)
    if err != nil {
        return nil, err
    }
    client.session = session

    return client, nil
}

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

ملاحظات:

  1. هذا ليس رمزًا مثاليًا ؛ أعيد الكثير من الأخطاء لأنها سهلة للغاية ، وهي بالضبط نوع الكود الذي لدينا مشاكل فيه. يجب تسجيل الاقتراحات من خلال الإيجاز ومدى سهولة إظهارها لإصلاح هذا الرمز.
  2. تستمر عبارة if forwardPort == 0 _ عمدًا_ من خلال الأخطاء ، ونعم ، هذا هو السلوك الحقيقي ، وليس شيئًا أضفته في هذا المثال.
  3. يُرجع هذا الرمز إما عميلاً صالحًا ومتصلًا أو يقوم بإرجاع خطأ وعدم وجود تسرب في الموارد ، وبالتالي فإن المعالجة حول. لاحظ أيضًا اختفاء الأخطاء من Close ، كما هو معتاد في Real Go.
  4. رقم المنفذ مقيد في مكان آخر ، لذلك لا يمكن أن يفشل url.Parse (عن طريق الفحص).

لن أدعي أن هذا يوضح كل سلوك خطأ محتمل ، لكنه يغطي سلسلة كاملة. (غالبًا ما أدافع عن Go on HN ومن خلال الإشارة إلى أنه بحلول الوقت الذي يتم فيه طهي الكود الخاص بي ، غالبًا ما يكون لدي _ جميع أنواع _ سلوكيات الخطأ في خوادم الشبكة الخاصة بي ؛ فحص كود الإنتاج الخاص بي ، بدءًا من 1/3 إلى أن نصف الأخطاء بالكامل فعلت شيئًا آخر غير مجرد إرجاعها.)

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

باستخدام try حيث يكون try مجرد اختصار لـ if! = nil return يقلل الكود بمقدار 6 سطور من 59 ، أي حوالي 10٪.

func NewClient(...) (*Client, error) {
    listener, err := net.Listen("tcp4", listenAddr)
    try err

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, err := ConnectionManager{}.connect(server, tlsConfig)
    try err

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    try err

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    try err

    session, err := communicationProtocol.FinalProtocol(conn)
    try err

    client.session = session

    return client, nil
}

والجدير بالذكر ، في عدة أماكن كنت أرغب في كتابة try x() ولكن لم أتمكن من ذلك لأنني كنت بحاجة إلى تعيين خطأ لكي يعمل المؤجلون بشكل صحيح.

مرة اخرى. إذا كان لدينا try شيء يمكن أن يحدث في سطور المهمة ، فإنه ينخفض ​​إلى 47 سطرًا.

func NewClient(...) (*Client, error) {
    try listener, err := net.Listen("tcp4", listenAddr)

    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    try conn, err := ConnectionManager{}.connect(server, tlsConfig)

    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    try session, err := communicationProtocol.FinalProtocol(conn)

    client.session = session

    return client, nil
}
import "github.com/pkg/errors"

func Func3() (T1, T2, error) {...}

type PathError {
    err Error
    x   T3
    y   T4
}

type MiscError {
    x   T5
    y   T6
    err Error
}


func Foo() (T1, T2, error) {
    // Old school
    a, b, err := Func(3)
    if err != nil {
        return nil
    }

    // Simplest form.
    // If last unhandled arg's type is same 
    // as last param of func,
    // then use anon variable,
    // check and return
    a, b := Func3()
    /*    
    a, b, err := Func3()
    if err != nil {
         return T1{}, T2{}, err
    }
    */

    // Simple wrapper
    // If wrappers 1st param TypeOf Error - then pass last and only unhandled arg from Func3() there
    a, b, errors.WithStack() := Func3() 
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithStack(err)
    }
    */

    // Bit more complex wrapper
    a, b, errors.WithMessage("unable to get a and b") := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, errors.WithMessage(err, "unable to get a and b")
    }
    */

    // More complex wrapper
    // If wrappers 1nd param TypeOf is not Error - then pass last and only unhandled arg from Func3() as last
    a, b, fmt.Errorf("at %v Func3() return error %v", time.Now()) := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, fmt.Errorf("at %v Func3() return error %v", time.Now(), err)
    }
    */

    // Wrapping with error types
    a, b, &PathError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &PathError{err, x, y}
    }
    */
    a, b, &MiscError{x,y} := Func3()
    /*
    a, b, err := Func3()
    if err != nil {
        return T1{}, T2{}, &MiscError{x, y, err}
    }
    */

    return a, b, nil
}

القليل من السحر (لا تتردد في -1) ولكنه يدعم الترجمة الميكانيكية

هذا هو الشكل (المحدث إلى حد ما) الذي سيبدو عليه اقتراحي:

func NewClient(...) (*Client, error) {
    defer annotateError("client couldn't be created")

    listener := pop net.Listen("tcp4", listenAddr)
    defer closeOnErr(listener)
    conn := pop ConnectionManager{}.connect(server, tlsConfig)
    defer closeOnErr(conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    pop toServer.Send(&client.serverConfig)
    pop toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    session := pop communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

func closeOnErr(c io.Closer) {
    if err := erroring(); err != nil {
        closeErr := c.Close()
        if err != nil {
            seterr(multierror.Append(err, closeErr))
        }
    }
}

func annotateError(annotation string) {
    if err := erroring(); err != nil {
        log.Printf("%s: %v", annotation, err)
        seterr(errwrap.Wrapf(annotation +": {{err}}", err))
    }
}

تعريفات:

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

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

يسمح لك seterr(error) بتغيير قيمة الخطأ التي يتم إرجاعها. سيكون هذا هو الخطأ الذي تراه أي مكالمات مستقبلية erroring() ، والذي كما هو موضح هنا يسمح بنفس التسلسل المستند إلى التأجيل الذي يمكن القيام به الآن.

أنا أستخدم غلاف hashicorp و multierror هنا ؛ أدخل الحزم الذكية الخاصة بك حسب الرغبة.

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

لاحظ أنني أترك التعامل مع forwardPort بمفرده ، بدلاً من محاولة تشويش بعض القواعد اللغوية حوله. كحالة استثنائية ، لا بأس في أن يكون هذا أكثر تفصيلاً.

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

يعمل هذا على توسيع أنماط Go الحالية مثل defer واستخدام الأخطاء كقيم لتسهيل أنماط معالجة الأخطاء الصحيحة التي يصعب التعبير عنها في بعض الحالات باستخدام Go أو الاستثناءات الحالية ، ولا تتطلب جراحة جذرية إلى وقت التشغيل (لا أعتقد) ، وأيضًا لا _تطلب _ a Go 2.0.

تشمل العيوب المطالبة بـ erroring و pop و seterr ككلمات رئيسية ، مع تكبد النفقات العامة لهذه الوظائف defer لهذه الوظائف ، حقيقة أن معالجة الأخطاء المعوملة لا تقفز حول بعض لوظائف المناولة ، وأنه لا يفعل أي شيء "لفرض" المعالجة الصحيحة. على الرغم من أنني لست متأكدًا من أن الأخير ممكن ، نظرًا لأن الشرط (الصحيح) للتوافق مع الإصدارات السابقة ، يمكنك دائمًا فعل الشيء الحالي.

مناقشة ممتعة للغاية هنا.

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

لذلك سأجعلها أكثر قابلية للقراءة باستخدام الكلمة الأساسية الموسعة "عودة؟". في C # ، تُستخدم علامة الاستفهام في بعض الأماكن لعمل اختصارات. E. ز. بدلا من الكتابة:

if(foo != null)
{ foo.Bar(); }

يمكنك فقط أن تكتب:
foo?.Bar();

لذلك بالنسبة إلى Go 2 ، أود أن أقترح هذا الحل:

func foobar() error {
    return fmt.Errorf("Some error happened")
}

// Implicitly return err (there must be exactly one error variable on the left side)
err := foobar() return?
// Explicitly return err
err := foobar() return? err
// Return extended error info
err := foobar() return? &PathError{"chdir", dir, err}
// Return tuple
err := foobar() return? -1, err
// Return result of function (e. g. for logging the error)
err := foobar() return? handleError(err)
// This doesn't compile as you ignore the error intentionally
foobar() return?

مجرد فكرة:

foo ، يخطئ: = myFunc ()
يخطئ! = لا شيء؟ عودة التفاف (يخطئ)

أو

إذا أخطأت! = لا شيء؟ عودة التفاف (يخطئ)

إذا كنت على استعداد لوضع بعض الأقواس المتعرجة حول ذلك ، فلا داعي لتغيير أي شيء!

if err != nil { return wrap(err) }

يمكنك الحصول على _ all_ المعالجة المخصصة التي تريدها (أو لا شيء على الإطلاق) ، وقد حفظت سطرين من التعليمات البرمجية من الحالة النموذجية ، وهي متوافقة مع الإصدارات السابقة بنسبة 100٪ (لأنه لا توجد تغييرات في اللغة) ، وهي أكثر إحكاما ، وهي سهل. إنه يضرب العديد من نقاط قيادة إيان . قد يكون التغيير الوحيد هو الأدوات gofmt ؟

لقد كتبت هذا قبل قراءة اقتراح carlmjohnson ، وهو مشابه ...

فقط # قبل الخطأ.

ولكن في تطبيق العالم الحقيقي ، لا يزال يتعين عليك كتابة if err != nil { ... } العادي حتى تتمكن من تسجيل الأخطاء ، وهذا يجعل الخطأ البسيط في التعامل مع غير مجدي ، إلا إذا كان بإمكانك إضافة برنامج وسيط عائد من خلال التعليقات التوضيحية المسماة after ، والذي يتم تشغيله بعد إرجاع الوظائف ... (مثل defer لكن مع args).

@after(func (data string, err error) {
  if err != nil {
    log.Error("error", data, " - ", err)
  }
})
func alo() (string, error) {
  // this is the equivalent of
  // data, err := db.Find()
  // if err != nil { 
  //   return "", err 
  // }
  str, #err := db.Find()

  // ...

  #err = db.Create()

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  // this is the equivalent of
  // data, data2, err, errNotFound := db.Find()
  // if err != nil { 
  //   return nil, nil, err, nil
  // } else if errNotFound != nil {
  //   return nil, nil, nil, errNotFound
  // }
  data, data2, #err, #errNotFound := db.Find()

  // ...

  return data, data2, nil, nil
}

أنظف من:

func alo() (string, error) {
  str, err := db.Find()
  if err != nil {
    log.Error("error on find in database", err)
    return "", err
  }

  // ...

  if err := db.Create(); err != nil {
    log.Error("error on create", err)
    return "", err
  }

  // ...

  return str, nil
}

func alo2() ([]byte, []byte, error, error) {
  data, data2, err, errNotFound := db.Find()
  if err != nil { 
    return nil, nil, err, nil
  } else if errNotFound != nil {
    return nil, nil, nil, errNotFound
  }

  // ...

  return data, data2, nil, nil
}

ماذا عن كشف حساب Swift مثل guard ، إلا أنه بدلاً من guard...else يكون guard...return :

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

أنا أحب توضيح معالجة أخطاء go. المشكلة الوحيدة ، imho ، هي مقدار المساحة التي تشغلها. أود أن أقترح تعديلين:

  1. السماح باستخدام العناصر التي لا يمكن استخدامها في سياق منطقي ، حيث nil false ، غير- nil إلى true
  2. دعم عوامل تشغيل العبارات الشرطية أحادية السطر ، مثل && و ||

لذا

file, err := os.Open("fails.txt")
if err != nil {
    return &FooError{"Couldn't foo fails.txt", err}
}

يمكن أن تصبح

file, err := os.Open("fails.txt")
if err {
    return &FooError{"Couldn't foo fails.txt", err}
}

أو حتى أقصر

file, err := os.Open("fails.txt")
err && return &FooError{"Couldn't foo fails.txt", err}

ويمكننا أن نفعل

i,ok := v.(int)
ok || return fmt.Errorf("not a number")

او ربما

i,ok := v.(int)
ok && s *= i

إذا كان التحميل الزائد على && و || يخلق الكثير من الغموض ، فربما يمكن تحديد بعض الأحرف الأخرى (لا تزيد عن 2) ، على سبيل المثال ? و # أو ?? و ## أو ?? و !! أو أيًا كان. الهدف هو دعم عبارة شرطية من سطر واحد مع الحد الأدنى من الأحرف "المزعجة" (لا حاجة لأقواس أو أقواس ، إلخ). عوامل التشغيل && و || جيدة لأن هذا الاستخدام له سابقة في لغات أخرى.

هذا ليس اقتراحًا لدعم التعبيرات الشرطية المعقدة أحادية السطر ، فقط الجمل الشرطية أحادية السطر.

أيضًا ، هذا ليس اقتراحًا لدعم مجموعة كاملة من "الصدق" لبعض اللغات الأخرى. ستدعم هذه الشروط فقط الصفري / غير الصفري ، أو القيم المنطقية.

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

لماذا لا تخترع عجلة وتستخدم النموذج المعروف try..catch كما قال mattn من قبل؟ )

try {
    a := foo() // func foo(string, error)
    b := bar() // func bar(string, error)
} catch (err) {
    // handle error
}

يبدو أنه لا يوجد سبب للتمييز بين مصدر الخطأ الذي تم اكتشافه ، لأنه إذا كنت بحاجة فعلاً إلى هذا ، فيمكنك دائمًا استخدام النموذج القديم if err != nil بدون try..catch .

أيضا ، أنا لست متأكدا حقا من ذلك ، ولكن يمكن إضافة القدرة على "طرح" خطأ إذا لم يتم التعامل معها؟

func foo() (string, error) {
    f := bar() // similar to if err != nil { return "", err }
}

func baz() string {
    // Compilation error.
    // bar's error must be handled because baz() does not return error.
    return bar()
}

gobwas من وجهة نظر goto . لا عجب أن اللغات الحديثة تحاول أن تكون صريحة بشأن ذلك وتتطلب من المبرمج تحديد الأماكن التي قد يتباعد فيها تدفق التحكم بسبب خطأ يتم طرحه. يشبه إلى حد كبير return أو goto ولكن مع بنية أجمل بكثير.

creker نعم ، أنا أتفق معك تمامًا. كنت أفكر في التحكم في التدفق في المثال أعلاه ، لكني لم أدرك كيفية القيام بذلك في شكل بسيط.

ربما شيء مثل:

try {
    a ::= foo() // func foo(string, error)
    b ::= bar() // func bar(string, error)
} catch (err) {
    // handle error
}

أو اقتراحات أخرى من قبل مثل try a := foo() ..؟

تضمين التغريدة

عندما أقوم بالهبوط في كتلة catch ، كيف أعرف الوظيفة الموجودة في كتلة try التي تسببت في حدوث الخطأ؟

urandom إذا كنت تريد أن تعرف ذلك ، فربما تريد أن تفعل if err != nil بدون try..catch .

@ robert-wallis: لقد ذكرت بيان Swift guard في وقت سابق في الموضوع ولكن الصفحة كبيرة جدًا لم يعد Github يقوم بتحميلها افتراضيًا. : -PI ما زلت أعتقد أن هذه فكرة جيدة ، وبشكل عام ، أؤيد البحث في لغات أخرى للحصول على أمثلة إيجابية / سلبية.

pdk

السماح باستخدام العناصر التي لا تسمح باستخدام nil في سياق منطقي ، حيث يساوي nil خطأ false ، و non-nil إلى true

أرى أن هذا يؤدي إلى الكثير من الأخطاء باستخدام حزمة العلم حيث يكتب الأشخاص if myflag { ... } لكنهم يقصدون كتابة if *myflag { ... } ولن يتم اكتشافها بواسطة المترجم.

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

FWIW ، تعمل محاولة Swift / catch على الأقل على حل المشكلة المرئية المتمثلة في عدم معرفة العبارات التي قد تؤدي إلى:

do {
    let dragon = try summonDefaultDragon() 
    try dragon.breathFire()
} catch DragonError.dragonIsMissing {
    // ...
} catch DragonError.halatosis {
    // ...
}

@ robert-wallis ، لديك مثال:

file, err := os.Open("fails.txt")
guard err return &FooError{"Couldn't foo fails.txt", err}

guard os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

عند أول استخدام لـ guard ، يبدو كثيرًا مثل if err != nil { return &FooError{"Couldn't foo fails.txt", err}} ، لذلك لست متأكدًا مما إذا كان هذا فوزًا كبيرًا.

في الاستخدام الثاني ، ليس من الواضح على الفور من أين يأتي err . يبدو أنه ما تم إرجاعه من os.Open ، والذي أعتقد أنه لم يكن هدفك؟ هل سيكون هذا أكثر دقة؟

guard err = os.Remove("fails.txt") return &FooError{"Couldn't remove fails.txt", err}

في هذه الحالة ، يبدو وكأنه ...

if err = os.Remove("fails.txt"); err != nil { return &FooError{"Couldn't remove fails.txt", err}}

لكنها لا تزال أقل فوضى بصرية. if err = ، ; err != nil { - حتى لو كانت بطانة واحدة ، فلا يزال هناك الكثير مما يحدث لمثل هذا الشيء البسيط

اتفق على أن هناك فوضى أقل. ولكن أقل بشكل ملحوظ من أجل ضمان الإضافة إلى اللغة؟ لست متأكدًا من أنني أوافق هناك.

أعتقد أن إمكانية قراءة كتل try-catch في Java / C # / ... جيدة جدًا حيث يمكنك اتباع تسلسل "المسار السعيد" دون أي مقاطعة من خلال معالجة الأخطاء. الجانب السلبي هو أن لديك آلية الانتقال المخفية.

في Go ، أبدأ بإدراج أسطر فارغة بعد معالج الأخطاء لجعل استمرار منطق "المسار السعيد" أكثر وضوحًا. إذن من هذه العينة من موقع golang.org (9 أسطر)

record := new(Record)
err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}
err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

أفعل ذلك غالبًا (11 سطرًا)

record := new(Record)

err := datastore.Get(c, key, record) 
if err != nil {
    return &appError{err, "Record not found", 404}
}

err := viewTemplate.Execute(w, record)
if err != nil {
    return &appError{err, "Can't display record", 500}
}

عد الآن إلى الاقتراح ، لأنني نشرت بالفعل شيئًا مثل هذا سيكون رائعًا (3 أسطر)

record := new(Record)
err := datastore.Get(c, key, record) return? &appError{err, "Record not found", 404}
err := viewTemplate.Execute(w, record) return? &appError{err, "Can't display record", 500}

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

سؤال موجه للجميع: هل يجب تجميع هذا الرمز؟

func foobar() error {
    return fmt.Errorf("Some error")
}
func main() {
    foobar()
}

IMHO يجب إجبار المستخدم على القول بأنه يتجاهل الخطأ عمدًا من خلال:

func main() {
    _ := foobar()
}

عند إضافة تقرير تجربة مصغرة متعلق بالنقطة 3 من منشورianlancetaylor الأصلي ، قم بإرجاع الخطأ بمعلومات سياقية إضافية .

عند تطوير مكتبة flac لـ Go ، أردنا إضافة معلومات سياقية إلى الأخطاء باستخدام حزمةdavecheney pkg / errors (https://github.com/mewkiz/flac/issues/22). وبشكل أكثر تحديدًا ، نقوم بلف الأخطاء التي تم إرجاعها باستخدام الأخطاء. WithStack الذي

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

type withStack struct {
    error
    *stack
}

الآن ، لاسترداد الخطأ الأصلي ، الاصطلاح هو استخدام الأخطاء . يتيح لك هذا مقارنة الخطأ الأصلي مقابل على سبيل المثال io.EOF .

يمكن لمستخدم المكتبة بعد ذلك كتابة شيء ما على غرار https://github.com/mewkiz/flac/blob/0884ed715ef801ce2ce0c262d1e674fdda6c3d94/cmd/flac2wav/flac2wav.go#L78 باستخدام errors.Cause للتحقق من الخطأ الأصلي القيمة:

frame, err := stream.ParseNext()
if err != nil {
    if errors.Cause(err) == io.EOF {
        break
    }
    return errors.WithStack(err)
}

يعمل هذا بشكل جيد في جميع الحالات تقريبًا.

عند إعادة هيكلة معالجة الأخطاء الخاصة بنا للاستفادة المتسقة من pkg / الأخطاء طوال الوقت للحصول على معلومات سياق إضافية ، واجهنا مشكلة خطيرة إلى حد ما. للتحقق من صحة الحشو الصفري ، قمنا بتطبيق io.Reader الذي يتحقق ببساطة مما إذا كانت وحدات البايت المقروءة صفرًا ، ويبلغ عن خطأ بخلاف ذلك. تكمن المشكلة في أنه بعد إجراء إعادة هيكلة آلية لإضافة معلومات سياقية إلى أخطائنا ، بدأت حالات الاختبار لدينا

كانت المشكلة أن النوع الأساسي للخطأ المُرجع بواسطة zeros.Read الآن هو أخطاء مع Stack ، بدلاً من io.EOF. وبالتالي ، تسببنا في حدوث مشكلات عندما استخدمنا ذلك القارئ مع io.Copy ، والذي يتحقق من io.EOF وجه التحديد ، ولا يعرف استخدام errors.Cause "لإلغاء" خطأ تم شرحه باستخدام معلومات سياقية. نظرًا لأنه لا يمكننا تحديث المكتبة القياسية ، كان الحل هو إرجاع الخطأ بدون معلومات مشروحة (https://github.com/mewkiz/flac/commit/6805a34d854d57b12f72fd74304ac296fd0c07be).

في حين أن فقدان المعلومات المشروحة للواجهات التي ترجع قيمًا ملموسة يعد خسارة ، فمن الممكن التعايش معها.

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

لكنها فعلت ، ولهذا السبب ، نرغب في المساهمة بتقرير تجربتنا للنظر فيه ؛ عند التفكير في كيفية دمج إضافة المعلومات السياقية في معالجة الأخطاء لـ Go 2 ، مثل أن مقارنة الأخطاء (كما هو مستخدم في عقود الواجهة) لا تزال قائمة.

بلطف،
روبن

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

لست على دراية بقاعدة الشفرات الخاصة بك ، وأدرك أنك قلت إنها كانت إعادة بناء ديون آلية ، ولكن لماذا احتجت إلى تضمين معلومات سياقية مع EOF؟ على الرغم من أنه يتم التعامل معها على أنها خطأ من قبل نظام النوع ، إلا أن EOF هي في الحقيقة قيمة إشارة وليست خطأ فعليًا. في تطبيق io.Reader وجه الخصوص ، إنها قيمة متوقعة في معظم الأوقات. أليس الإصلاح الأفضل هو التفاف الخطأ فقط إذا لم يكن io.EOF ؟

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

أعني ألا تحاول المحاولة في الأساس نفس سلوك الذعر () والتعافي () على أي حال؟

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

_, ? := foo()
x?, err? := bar()

أو ربما شيء من هذا القبيل

_, err := foo(); return err?
x, err := bar(); return x? || err?
x, y, err := baz(); return (x? && y?) || err?

أين ال ؟ يصبح اسمًا مستعارًا لاختصارًا لـ if var! = nil {return var}.

يمكننا حتى تحديد واجهة مدمجة خاصة أخرى ترضيها الطريقة

func ?() bool //looks funky but avoids breakage.

يمكننا استخدامها لتجاوز السلوك الافتراضي للمشغل الشرطي الجديد والمحسن.

تضمين التغريدة

أعتقد أنني أوافق.
إذا كانت المشكلة تتمثل في الحصول على طريقة لطيفة لتقديم المسار السعيد ، فيمكن لمكوِّن إضافي لـ IDE طي / كشف كل مثيل if err != nil { return [...] } باختصار؟

أشعر أن كل جزء مهم الآن. err != nil مهم. return ... مهم.
من الصعب بعض الشيء الكتابة ، لكن يجب كتابتها. وهل هو حقا يبطئ الناس؟ ما يستغرق وقتًا هو التفكير في الخطأ وما يجب إرجاعه وليس كتابته.

سأكون أكثر اهتمامًا بالاقتراح الذي يسمح بتحديد نطاق المتغير err .

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

لا أرى كيف يمكن أن يعمل هذا:

x ، y ، يخطئ: = baz () ؛ العودة ( x? && y? ) || err?

أين ال ؟ يصبح اسمًا مستعارًا لاختصارًا لـ if var == nil {return var}.

x ، y ، يخطئ: = baz () ؛ العودة ( if x == nil{ return x} && if y== nil{ return y} ) || if err == nil{ return err}

x ، y ، يخطئ: = baz () ؛ إرجاع (x؟ && y؟) || يخطئ؟

يصبح

x ، y ، يخطئ: = baz () ؛
إذا ((x! = nil && y! = nil) || err! = nil)) {
العودة س ، ص ، يخطئ
}

عندما ترى س؟ && ذ؟ || يخطئ؟ يجب أن تفكر في "هل س وص صحيح؟ وماذا عن يخطئ؟"

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

أقترح Go إضافة معالجة الأخطاء الافتراضية في Go الإصدار 2.

إذا لم يتعامل المستخدم مع الخطأ ، فسيقوم المترجم بإرجاع الخطأ إذا لم يكن لا شيء ، لذلك إذا كتب المستخدم:

func Func() error {
    func1()
    func2()
    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

ترجمة تحويله إلى:

func Func() error {
    err := func1()
    if err != nil {
        return err
    }

    err = func2()
    if err != nil {
        return err
    }

    return nil
}

func func1() error {
    ...
}

func func2() error {
    ...
}

إذا تعامل المستخدم مع الخطأ أو تجاهله باستخدام _ ، فلن يفعل المترجم شيئًا:

_ = func1()

أو

err := func1()

لقيم إرجاع متعددة ، فهو مشابه:

func Func() (*YYY, error) {
    ch, x := func1()
    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

سوف يتحول المترجم إلى:

func Func() (*YYY, error) {
    ch, x, err := func1()
    if err != nil {
        return nil, err
    }

    return yyy, nil
}

func func1() (chan int, *XXX, error) {
    ...
}

إذا لم يُرجع توقيع Func () خطأ ولكن استدعاء الوظائف التي ترجع الخطأ ، فسيقوم المحول البرمجي بالإبلاغ عن الخطأ: "يرجى معالجة الخطأ في Func ()"
ثم يمكن للمستخدم فقط تسجيل الخطأ في Func ()

وإذا أراد المستخدم التفاف بعض المعلومات بالخطأ:

func Func() (*YYY, error) {
    ch, x := func1() ? Wrap(err, "xxxxx", ch, "abc", ...)
    return yyy, nil
}

أو

func Func() (*YYY, error) {
    ch, x := func1() ? errors.New("another error")
    return yyy, nil
}

الفائدة هي ،

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

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

أعتقد أن وضع معالجة الخطأ في صيغة المهمة لا يحل جذر المشكلة ، وهي "معالجة الأخطاء متكررة".

استخدام if (err != nil) { return nil } (أو ما شابه) بعد العديد من أسطر التعليمات البرمجية (حيث يكون من المنطقي القيام بذلك) يتعارض مع مبدأ DRY (لا تكرر نفسك). أعتقد أن هذا هو سبب عدم إعجابنا بهذا.

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

لا أعتقد أن هذه متنافية ؛ يمكننا الحصول على نوع من try...catch بدون throws .

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

هذا ما أقترحه:

  • باستخدام ? كعنصر نائب لخطأ تم إرجاعه ، حيث سيتم استخدام _ لتجاهله
  • بدلاً من catch كما في المثال أدناه ، يمكن استخدام error? بدلاً من ذلك للتوافق الكامل مع الإصدارات السابقة

^ إذا كان افتراضي أن هذه متوافقة مع الإصدارات السابقة غير صحيح ، فيرجى ذكرها.

func example() {
    {
        // The following line will invoke the catch block
        data, ? := foopkg.buyIt()
        // The next two lines handle an error differently
        otherData, err := foopkg.useIt()
        if err != nil {
            // Here we eliminated deeper indentation
            otherData, ? = foopkg.breakIt()
        }
        if data == "" || otherData == "" {
        }
    } catch (err) {
        return errors.Label("fix it", err)
        // Aside: why not add Label() to the error package?
    }
}

فكرت في حجة ضد هذا: إذا قمت بكتابته بهذه الطريقة ، فإن تغيير كتلة catch قد يكون له تأثيرات غير مقصودة على الكود في نطاقات أعمق. هذه هي نفس المشكلة التي نواجهها مع try...catch .

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

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

تحرير 2: فكرة مجنونة: ربما لن تؤثر كتلة catch على تدفق التحكم ... سيكون حرفيا مثل نسخ ولصق الكود داخل catch { ... } إلى السطر بعد الخطأ ? ed (حسنًا ، ليس تمامًا - سيظل له نطاقه الخاص). يبدو الأمر غريبًا نظرًا لعدم اعتياد أي منا على ذلك ، لذلك لا ينبغي بالتأكيد أن تكون الكلمة الرئيسية catch إذا تم ذلك بهذه الطريقة ، ولكن بخلاف ذلك ... لماذا لا؟

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

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

jba هل تعرف أي مشكلة مخصصة على وجه التحديد لتغليف / فك تغليف المعلومات السياقية للأخطاء؟

لست على دراية بقاعدة الشفرات الخاصة بك ، وأدرك أنك قلت إنها كانت إعادة بناء ديون آلية ، ولكن لماذا احتجت إلى تضمين معلومات سياقية مع EOF؟ على الرغم من أنه يتم التعامل معها على أنها خطأ من قبل نظام النوع ، إلا أن EOF هي في الحقيقة قيمة إشارة وليست خطأ فعليًا. في تطبيق io.Reader على وجه الخصوص ، إنها قيمة متوقعة في معظم الأوقات. ألم يكن الحل الأفضل هو التفاف الخطأ فقط إذا لم يكن io.EOF؟

DeedleFake يمكنني التوضيح قليلاً ، ولكن للبقاء على الموضوع سوف أفعل ذلك في المشكلة المذكورة أعلاه المخصصة لتغليف / فك التفاف المعلومات السياقية للأخطاء.

كلما قرأت جميع المقترحات (بما في ذلك المقترحات) كلما قلت أعتقد أننا نواجه مشكلة في معالجة الأخطاء أثناء التنقل.

ما أرغب فيه هو بعض الإنفاذ لعدم تجاهل قيمة إرجاع الخطأ عن طريق الخطأ ولكن فرضه على الأقل
_ := returnsError()

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

يبدو أن المشكلة الرئيسية التي يحاول معظم الناس حلها هنا هي "مقدار الكتابة" أو "عدد الأسطر". أنا أتفق مع أي بناء جملة يقلل من عدد الأسطر ، ولكن هذا في الغالب مشكلة gofmt. ما عليك سوى السماح بـ "نطاقات سطر واحد" مضمنة ونحن جيدون.

اقتراح آخر لحفظ بعض الكتابة هو التحقق الضمني من nil كما هو الحال مع القيم المنطقية:

err := returnsError()
if err { return err }

او حتى

if err := returnsError(); err { return err }

هذا من شأنه أن يعمل مع جميع أنواع مؤشرات السبب.

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

تعليمات برمجية أقل قابلية للقراءة وبناء جملة أكثر تعقيدًا.

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

نمط ظهور فقاعات خطأ وإعادة القيمة الصفرية لكل شيء

19642 سيجعل هذا أسهل.


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

لا أشعر أنني شرحت فكرتي جيدًا ، لذلك قمت بإنشاء جوهر (وقمت بمراجعة العديد من أوجه القصور التي لاحظتها للتو)

https://gist.github.com/KernelDeimos/384aabd36e1789efe8cbce3c17ffa390

هناك أكثر من فكرة في هذا الجوهر ، لذا آمل أن تتم مناقشتها بشكل منفصل عن بعضها البعض

لنضع جانباً للحظة فكرة أن الاقتراح هنا يجب أن يكون صريحًا حول معالجة الأخطاء ، ماذا لو قدم Go شيئًا مثل عبارة collect ؟

قد تكون عبارة collect على شكل collect [IDENT] [BLOCK STMT] ، حيث يجب أن يكون ident متغيرًا في النطاق من النوع nil -able. ضمن عبارة collect ، يتوفر متغير خاص _! كاسم مستعار للمتغير الذي يتم تجميعه إليه. لا يمكن استخدام _! أي مكان ولكن كمهمة ، مثل _ . عندما يتم تعيين _! إلى ، يتم إجراء شيك ضمني nil ، وإذا لم يكن _! صفريًا ، فإن الكتلة تتوقف عن التنفيذ وتستمر مع باقي الكود.

من الناحية النظرية ، سيبدو هذا شيئًا كالتالي:

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    collect err {
        intermediate1, _! := Step1()
        intermediate2, _! := Step2(intermediate1, "something")
        // assign to result from the outer scope
        result, _! = Step3(intermediate2, 12)
    }
    return result, err
}

وهو ما يعادل

func TryComplexOperation() (*Result, error) {
    var result *Result
    var err error

    {
        var intermediate1 SomeType
        intermediate1, err = Step1()
        if err != nil { goto collectEnd }

        var intermediate2 SomeOtherType
        intermediate2, err = Step2(intermediate1, "something")
        if err != nil { goto collectEnd }

        result, err = Step3(intermediate2, 12)
        // if err != nil { goto collectEnd }, but since we're at the end already we can omit this
    }

collectEnd:
    return result, err
}

بعض الأشياء الأخرى اللطيفة التي تتيحها ميزة بناء الجملة مثل هذه:

// try several approaches for acquiring a value
func GetSomething() (s *Something) {
    collect s {
        _! = fetchOrNil1()
        _! = fetchOrNil2()
        _! = new(Something)
    }
    return s
}

ميزات بناء الجملة الجديدة المطلوبة:

  1. الكلمة الأساسية collect
  2. هوية خاصة _! (لقد لعبت مع هذا في المحلل اللغوي ، ليس من الصعب جعل هذه المباراة كهوية دون كسر أي شيء آخر)

السبب في أنني أقترح شيئًا كهذا هو أن حجة "معالجة الأخطاء متكررة للغاية" يمكن اختزالها إلى "عدم وجود عمليات تحقق متكررة للغاية". يحتوي Go بالفعل على الكثير من ميزات معالجة الأخطاء التي تعمل كما هي. يمكنك تجاهل خطأ في _ (أو عدم التقاط قيم الإرجاع فقط) ، يمكنك إرجاع خطأ غير معدّل بـ if err != nil { return err } ، أو إضافة سياق والعودة بـ if err != nil { return wrap(err) } . لا توجد أي من هذه الأساليب ، بمفردها ، متكررة للغاية. يأتي التكرار (من الواضح ) من الاضطرار إلى تكرار هذه العبارات النحوية أو ما شابهها في جميع أنحاء الكود. أعتقد أن تقديم طريقة لتنفيذ العبارات حتى يتم العثور على قيمة non-Nil هو طريقة جيدة للحفاظ على معالجة الخطأ ، ولكن تقليل كمية البيانات المعيارية اللازمة للقيام بذلك.

  • دعم جيد لـ 1) تجاهل خطأ ؛ 3) تغليف خطأ بسياق إضافي.

تحقق ، لأنها تظل كما هي (في الغالب)

  • بينما يجب أن يكون رمز معالجة الأخطاء واضحًا ، يجب ألا يهيمن على الوظيفة.

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

  • يجب أن يستمر رمز Go 1 الحالي في العمل ، أو على الأقل يجب أن يكون من الممكن ترجمة Go 1 ميكانيكيًا إلى النهج الجديد بموثوقية كاملة.

تحقق ، هذه إضافة وليست تعديلًا

  • يجب أن يشجع النهج الجديد المبرمجين على معالجة الأخطاء بشكل صحيح.

تحقق ، على ما أعتقد ، نظرًا لأن آليات معالجة الأخطاء ليست مختلفة - سيكون لدينا فقط بناء جملة "لتجميع" أول قيمة غير صفرية من سلسلة من عمليات التنفيذ والواجبات ، والتي يمكن استخدامها للحد من عدد الأماكن التي يتعين علينا كتابة كود معالجة الأخطاء فيها في دالة

  • يجب أن يكون أي نهج جديد أقصر و / أو أقل تكرارًا من النهج الحالي ، مع الحفاظ على الوضوح.

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

  • اللغة تعمل فعلاً اليوم ، وكل تغيير له تكلفة. لا ينبغي أن يكون مجرد غسيل ، بل يجب أن يكون أفضل بشكل واضح.

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

KernelDeimos لقد x, ? := doSomething() بشكل جيد في الممارسة العملية. على الرغم من أنه من الرائع أن أرى أنني لست الشخص الوحيد الذي يفكر في إضافة؟ عامل في اللغة بطريقة مثيرة للاهتمام.

https://github.com/golang/go/issues/25582

أليس هذا في الأساس مجرد فخ ؟

هذه هي لعبة spitball:

func NewClient(...) (*Client, error) {
    trap(err error) {
        return nil, err
    }

    listener, err? := net.Listen("tcp4", listenAddr)
    trap(_ error) {
        listener.Close()
    }

    conn, err? := ConnectionManager{}.connect(server, tlsConfig)
    trap(_ error) {
        conn.Close()
    }

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?

    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?

    session, err? := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

59 سطرًا ← 44

يعني trap "تشغيل هذا الرمز بترتيب مكدس إذا لم يكن المتغير الذي تم تمييزه بـ ? من النوع المحدد هو القيمة الصفرية." إنه يشبه defer ولكنه قد يؤثر على تدفق التحكم.

تعجبني فكرة trap ، لكن بناء الجملة يزعجني قليلاً. ماذا لو كان نوع من التصريح؟ على سبيل المثال ، يصرح trap err error {} عن trap يسمى err من النوع error والذي ، عند تعيينه ، يقوم بتشغيل الكود المحدد. ليس من الضروري أن يعود الرمز ؛ من المسموح له فقط القيام بذلك. هذا أيضًا يكسر الاعتماد على nil كونه خاصًا.

تحرير: التوسيع وإعطاء مثال الآن أنني لست على الهاتف.

func Example(r io.Reader) error {
  trap err error {
    if err != nil {
      return err
    }
  }

  n, err? := io.Copy(ioutil.Discard, r)
  fmt.Printf("Read %v bytes.\n", n)
}

بشكل أساسي ، يعمل trap تمامًا مثل var ، باستثناء أنه عندما يتم تعيينه مع عامل التشغيل ? المرفق به ، يتم تنفيذ كتلة التعليمات البرمجية. عامل التشغيل ? يمنعه أيضًا من التظليل عند استخدامه مع := . يُسمح بإعادة إعلان trap في نفس النطاق ، على عكس var ، لكن يجب أن يكون من نفس النوع الموجود ؛ هذا يسمح لك بتغيير كتلة التعليمات البرمجية المرتبطة. نظرًا لأن الكتلة التي يتم تشغيلها لا تعود بالضرورة ، فهي تتيح لك أيضًا أن يكون لديك مسارات منفصلة لأشياء معينة ، مثل التحقق مما إذا كان err == io.EOF .

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

carlmjohnson على من كنت ترد؟
بغض النظر ، يبدو أن هذا المفهوم trap مجرد طريقة مختلفة لكتابة عبارة defer ، أليس كذلك؟ سيكون الرمز ، كما هو مكتوب ، هو نفسه بشكل أساسي إذا كنت panic 'd على خطأ غير صفري ثم استخدمت الإغلاق المؤجل لتعيين قيم الإرجاع المسماة وإجراء التنظيف. أعتقد أن هذا يواجه نفس المشكلات مثل اقتراحي السابق باستخدام _! للذعر تلقائيًا ، لأنه يضع طريقة معالجة خطأ واحدة على أخرى. FWIW شعرت أيضًا أن التفكير في الكود ، كما هو مكتوب ، كان أصعب بكثير من التفكير في الكود الأصلي. هل يمكن محاكاة مفهوم trap هذا باستخدام go اليوم ، حتى لو كان أقل وضوحًا من وجود بناء جملة له؟ أشعر أنه يمكن ذلك وسيكون الأمر if err != nil { panic (err) } و defer لالتقاط ذلك والتعامل معه.

يبدو مشابهًا لمفهوم الكتلة collect اقترحتها أعلاه ، والتي أشعر شخصيًا أنها توفر طريقة أوضح للتعبير عن نفس الفكرة ("إذا لم تكن هذه القيمة صفرية ، فأنا أريد التقاطها والقيام بشيء ما معها "). الذهاب يحب أن يكون خطيًا وصريحًا. trap وكأنه بناء جملة جديد لـ panic / defer لكن بتدفق تحكم أقل وضوحًا.

mccolljr ، يبدو أنه رد علي . أستنتج من مشاركتك أنك لم ترى اقتراحي (الآن في "العناصر المخفية" ، على الرغم من عدم وجود ذلك بعيدًا) ، لأنني استخدمت بالفعل بيانًا مؤجلًا في اقتراحي ، مع تمديد بـ recover - مثل وظيفة لمعالجة الخطأ.

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

القدرة على متابعة التدفق مسموح بها من خلال المثال trap المعدل الذي قدمته أعلاه . لقد قمت بتحريره لاحقًا ، لذلك لا أعرف ما إذا كنت رأيته أم لا. إنه مشابه جدًا لـ collect ، لكنني أعتقد أنه يمنح قدرًا أكبر من التحكم فيه. اعتمادًا على كيفية عمل قواعد تحديد النطاق لها ، يمكن أن تكون معكرونة قليلاً ، لكنني أعتقد أنه سيكون من الممكن إيجاد توازن جيد.

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

DeedleFake نفس الشيء ينطبق على trap ، لأن الطريقة التي أراها trap هي إما بشكل أساسي ماكرو يقوم بإدراج رمز عند استخدام عامل التشغيل ? والذي يقدم مجموعة من الاهتمامات الخاصة به والاعتبارات ، أو يتم تنفيذه على أنه goto ... وماذا لو لم يعد المستخدم في الكتلة trap ، أو كان مجرد اختلاف بنائي defer . أيضًا ، ماذا لو أعلنت عدة كتل ملائمة في دالة؟ هل هذا مسموح؟ إذا كان الأمر كذلك ، فمن سيتم إعدامه؟ يضيف ذلك تعقيد التنفيذ. يذهب يحب أن يكون آرائه ، وأنا أحب ذلك في ذلك. أعتقد أن collect أو بنية خطية مماثلة تتماشى مع أيديولوجية Go أكثر من trap ، والتي ، كما أشرت لي بعد اقتراحي الأول ، يبدو أنها try-catch بناء في زي.

ماذا لو لم يعد المستخدم في كتلة المصيدة

إذا لم يتم إرجاع trap أو تعديل تدفق التحكم ( goto ، continue ، break ، وما إلى ذلك) ، يعود تدفق التحكم إلى مكان الرمز تم "استدعاء" الكتلة من. ستعمل الكتلة نفسها بشكل مشابه لاستدعاء الإغلاق ، باستثناء أن لديها إمكانية الوصول إلى آليات التحكم في التدفق. ستعمل الآليات في المكان الذي تم الإعلان عن الكتلة ، وليس في المكان الذي يتم استدعاؤها منه ، هكذا

for {
  trap err error {
    break
  }

  err? = errors.New("Example")
}

سيعمل.

أيضًا ، ماذا لو أعلنت عدة كتل ملائمة في دالة؟ هل هذا مسموح؟ إذا كان الأمر كذلك ، فمن سيتم إعدامه؟

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

trap err error {
  // Block 1.
}

trap n int {
  // Block 2.
}

n? = 3

يتم استدعاء الكتلة 2. من المحتمل أن يكون السؤال الكبير في هذه الحالة هو ما يحدث في حالة n?, err? = 3, errors.New("Example") ، والتي قد تتطلب على الأرجح تحديد ترتيب المهام ، كما جاء في # 25609.

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

أعتقد أن كلا من collect و trap هما أساسًا try-catch s في الاتجاه المعاكس. المعيار try-catch هو سياسة فشل افتراضيًا تتطلب منك التحقق منها أو تنفجر. هذا نظام ناجح افتراضيًا يسمح لك بتحديد مسار الفشل بشكل أساسي.

الشيء الوحيد الذي يعقد الأمر برمته هو حقيقة أن الأخطاء لا يتم التعامل معها بطبيعتها على أنها فشل ، وأن بعض الأخطاء ، مثل io.EOF ، لا تحدد الفشل على الإطلاق. أعتقد أن هذا هو السبب في أن الأنظمة غير المرتبطة بالأخطاء على وجه التحديد ، مثل collect أو trap ، هي السبيل للذهاب.

"آه ، هذا منطقي أكثر. لم أدرك أنه كان ردًا على اقتراحك. ومع ذلك ، ليس من الواضح بالنسبة لي الفرق بين الخطأ () والاسترداد () ، بصرف النظر عن حقيقة أن الاسترداد يستجيب لـ هلع."

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

واحدة من نقاط اقتراحي هو استكشاف أبعد من مجرد "ماذا لو نصلح هذه القطعة المتكررة من ثلاثة خطوط حيث أننا return err واستبدالها ? " إلى "كيف يمكن أن تؤثر على بقية اللغة؟ ما هي الأنماط الجديدة التي تتيحها؟ ما هي "أفضل الممارسات" الجديدة التي تخلقها؟ ما هي "أفضل الممارسات" القديمة التي لم تعد تمثل أفضل الممارسات؟ " أنا لا أقول أنني أنهيت هذه الوظيفة. وحتى إذا تم الحكم عليها ، فإن الفكرة لديها بالفعل الكثير من القوة بالنسبة لمذاق Go (نظرًا لأن Go ليست لغة تعظيم القوة ، وحتى مع اختيار التصميم لقصرها على كتابة error فهي لا تزال على الأرجح الأقوى الاقتراح المقدم في هذا الموضوع ، والذي أعنيه تمامًا بالمعنى الجيد والسيئ لـ "القوي") ، أعتقد أنه يمكننا الوقوف لاستكشاف الأسئلة حول ما ستفعله التركيبات الجديدة للبرامج ككل ، بدلاً من ما ستفعل لسبعة وظائف من أمثلة الأسطر ، وهذا هو السبب في أنني حاولت على الأقل إحضار الأمثلة حتى 50-100 سطر من نطاق "الكود الحقيقي". يبدو كل شيء كما هو في 5 أسطر ، والتي تتضمن معالجة أخطاء Go 1.0 ، والتي ربما تكون جزءًا من السبب الذي يجعلنا نعلم جميعًا من تجاربنا الخاصة أن هناك مشكلة حقيقية هنا ، ولكن المحادثة تدور في دوائر إذا تحدثنا عنها على نطاق صغير جدًا حتى يبدأ بعض الأشخاص في إقناع أنفسهم ، فربما لا توجد مشكلة بعد كل شيء. (ثق بتجاربك الحقيقية في الترميز ، وليس العينات المكونة من 5 أسطر!)

"يبدو أننا سنفعل بشكل ضمني نوعًا من الذعر عندما يحتاج الأمر إلى إرجاع خطأ."

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

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

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

لقد أقرت بذلك بالفعل. على الرغم من أنني أقترح أنه بشكل عام ليست باهظة الثمن ولا أشعر بالسوء حيال قول ذلك لأغراض التحسين إذا كان لديك وظيفة ساخنة ، فاكتبها بالطريقة الحالية. ليس هدفي صراحة محاولة تعديل 100٪ من جميع وظائف معالجة الأخطاء ، ولكن جعل 80٪ منها أبسط وأكثر صحة وترك 20٪ حالات (ربما بصراحة أكثر مثل 98/2 ، بصراحة) تبقى كما هي نكون. الجزء الأكبر من كود Go ليس حساسًا لاستخدام التأجيل ، وهو ، بعد كل شيء ، سبب وجود defer في المقام الأول.

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

فقط لتوضيح أن إضافة كلمة رئيسية جديدة إلى اللغة هو تغيير جذري.

package main

import (
    "fmt"
)

func return(i int)int{
    return i
}

func main() {
    return(1)
}

النتائج في

prog.go:7:6: syntax error: unexpected select, expecting name or (

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

لهذا السبب اقترحت في البداية إضافة عامل تشغيل خاص ? go يمكن تطبيقه على المتغيرات في سياق البيانات. تم تعيين الحرف ? اعتبارًا من الآن كحرف غير قانوني لأسماء المتغيرات. مما يعني أنه ليس قيد الاستخدام حاليًا في أي كود Go حالي ، وبالتالي يمكننا تقديمه دون تكبد أي تغييرات عكسية.

الآن مشكلة استخدامه في الجانب الأيسر من المهمة هو أنه لا يأخذ في الاعتبار أن Go يسمح بوسائط إرجاع متعددة.

على سبيل المثال ضع في اعتبارك هذه الوظيفة

func getCoord() x int, y int, z int, err error{
    x, err = getX()
    if err != nil{
        return 
    }

    y, err = getY()
    if err != nil{
        return 
    }

    z, err = getZ()
        if err != nil{
        return 
    }
    return
}

إذا استخدمنا؟ أو حاول في lhs من التخصيص للتخلص من كتل if err! = nil ، هل نفترض تلقائيًا أن الأخطاء تعني أن جميع القيم الأخرى أصبحت الآن مهملات؟ ماذا لو فعلناها هكذا

func GetCoord() (x, y, z int, err error) {
    err = try GetX(&x) // or err? = GetX(&x) 
    err = try GetY(&y) // or err? = GetY(&x) 
    err = try GetZ(&z) // or err? = GetZ(&x) 
}

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

TLDR ؛ مضيفا؟ أو try للتخصيصات في محاولة للتخلص منها

if err != nil{
    return err
}

يقدم الكثير من الالتباس أكثر من الامتيازات.

وإضافة شيء مثل اقتراح trap يقدم إمكانية الكسر.

وهذا هو السبب في أنني قدمت في اقتراحي في قضية منفصلة. لقد سمحت بالقدرة على التصريح بـ func ?() bool على أي نوع حتى عندما اتصلت بـ say

x, err := doSomething; return x, err?    

يمكن أن يحدث هذا التأثير الجانبي للمصيدة بطريقة تنطبق على أي نوع.

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

switch {
    case select?:
    //side effect/trap code specific to select
    case return?:
    //side effect/trap code specific to returns
    case for?: 
    //side effect/trap code specific to for? 

    //etc...
}  

إذا كنا نستخدم؟ على نوع ليس به صريح؟ تم الإعلان عن وظيفة أو نوع مضمن ، ثم السلوك الافتراضي للتحقق مما إذا كانت var == nil || القيمة الصفرية {تنفيذ البيان} هي النية المفترضة.

أنا لست خبيرًا في تصميم لغة البرمجة ، لكن ليس هذا

على سبيل المثال ، وظيفة os.Chdir هي حاليًا

func Chdir(dir string) error {
  if e := syscall.Chdir(dir); e != nil {
      return &PathError{"chdir", dir, e}
  }
  return nil
}

بموجب هذا الاقتراح ، يمكن كتابته كـ

func Chdir(dir string) error {
  syscall.Chdir(dir) || &PathError{"chdir", dir, err}
  return nil
}

في الأساس نفس الشيء مثل وظائف سهم جافا سكريبت أو كما يعرّفها دارت "بنية السهم السمين"

على سبيل المثال

func Chdir(dir string) error {
    syscall.Chdir(dir) => &PathError{"chdir", dir, err}
    return nil
}

من جولة السهام .

بالنسبة للوظائف التي تحتوي على تعبير واحد فقط ، يمكنك استخدام صيغة مختصرة:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
صيغة => expr هي اختصار لـ {return expr؛ }. يشار أحيانًا إلى التدوين => باسم بناء جملة السهم السميك.

mortdeus ، الجانب الأيسر من سهم Dart هو توقيع دالة ، بينما syscall.Chdir(dir) هو تعبير. يبدو أنهم غير مرتبطين إلى حد ما.

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

ماذا عن كشف حساب جديد مشروط return (أو returnIf

return(bool expression) ...

بمعنى آخر.

err := syscall.Chdir(dir)
return(err != nil) &PathError{"chdir", dir, e}

a, b, err := Foo()    // signature: func Foo() (string, string, error)
return(err != nil) "", "", err

أو اسمح فقط لـ fmt بتنسيق وظائف إرجاع الأسطر الواحدة في سطر واحد بدلاً من ثلاثة:

err := syscall.Chdir(dir)
if err != nil { return &PathError{"chdir", dir, e} }

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil { return "", "", err }

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

func NewClient(...) (c *Client, err error) {
    defer annotateError(&err, "client couldn't be created")

    listener := net.Listen("tcp4", listenAddr)?
    defer closeOnErr(&err, listener)
    conn := ConnectionManager{}.connect(server, tlsConfig)?
    defer closeOnErr(&err, conn)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
         forwardOut = forwarding.NewOut(pop url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort)))
    }

    client := &Client{listener: listener, conn: conn, forward: forwardOut}

    toServer := communicationProtocol.Wrap(conn)
    toServer.Send(&client.serverConfig)?
    toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})?
    session := communicationProtocol.FinalProtocol(conn)?
    client.session = session

    return client, nil
}

func closeOnErr(err *error, c io.Closer) {
    if *err != nil {
        closeErr := c.Close()
        if err != nil {
            *err = multierror.Append(*err, closeErr)
        }
    }
}

func annotateError(err *error, annotation string) {
    if *err != nil {
        log.Printf("%s: %v", annotation, *err)
        *err = errwrap.Wrapf(annotation +": {{err}}", err)
    }
}

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

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

على الرغم من أنني أود أن أعبر عن تفضيل لاقتراح يسمح باستخدام وظيفة التوقيع func GetInt() (x int, err error) في الكود مع OtherFunc(GetInt()?, "...") (أو أيًا كانت النتيجة النهائية) إلى اقتراح لا يمكن تكوينه فيه تعبير. بينما أقل إزعاجًا لشرط معالجة الخطأ البسيط المتكرر المستمر ، فإن مقدار الكود الخاص بي الذي يفكك دالة arity 2 فقط حتى تحصل على النتيجة الأولى لا يزال مهمًا بشكل مزعج ، ولا يضيف حقًا أي شيء إلى وضوح الكود الناتج.

thejerf ، أشعر أن هناك الكثير من السلوك الغريب هنا. أنت تتصل بـ net.Listen ، وهو ما يعرض خطأ ، لكن لم يتم تعيينه. ثم تأجيل ، تمرير err . هل كل جديد defer يتجاوز آخر واحد ، بحيث لا يتم استدعاء annotateError مطلقًا؟ أم أنها تتكدس ، بحيث إذا تم إرجاع خطأ من ، على سبيل المثال ، toServer.Send ، ثم يتم استدعاء closeOnErr مرتين ، ثم يتم استدعاء annotateError ؟ هل يتم استدعاء closeOnErr فقط إذا كانت المكالمة السابقة لها توقيع مطابق؟ ماذا عن هذه الحالة؟

conn := ConnectionManager{}.connect(server, tlsConfig)?
fmt.Printf("Attempted to connect to server %#v", server)
defer closeOnErr(&err, conn)

قراءة الكود تربك الأشياء أيضًا ، مثل لماذا لا أستطيع أن أقول فقط

client.session = communicationProtocol.FinalProtocol(conn)?

من المفترض ، لأن FinalProtocol يقوم بإرجاع خطأ؟ لكن هذا مخفي عن القارئ.

أخيرًا ، ماذا يحدث عندما أريد الإبلاغ عن خطأ والتعافي منه داخل دالة؟ يبدو أن مثالك سيمنع هذه الحالة؟

_إضافة_

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

env, err := environment.GetRuntimeEnvironment()

هذا جيد لأن الخطأ مظلل ، لكن بعد ذلك إذا تغيرت ...

forwardPort, err = env.PortToForward()
if err != nil {
    log.Printf("env couldn't provide forward port: %v", err)
}

لمجرد

forwardPort = env.PortToForward()

بعد ذلك ، لن يتم التعامل مع الخطأ المؤجل الخاص بك ، لأنك تستخدم err تم إنشاؤه في النطاق. أم هل فاتني شيء؟

أعتقد أن إضافة إلى بناء الجملة تشير إلى أن الوظيفة يمكن أن تفشل هي بداية جيدة. أقترح شيئًا من هذا القبيل:

func (r Reader) Read(b []byte) (n int) fails {
    if somethingFailed {
        fail errors.New("something failed")
    }

    return 0
}

إذا فشلت إحدى الوظائف (باستخدام الكلمة الأساسية fail بدلاً من return ، فإنها تُرجع القيمة الصفرية لكل معلمة إرجاع.

func (c EmailClient) SendEmail(to, content string) fails {
    if !c.connected() {
        fail errors.New("could not connect")
    }

    // You can handle it and execution will continue if you don't fail or return
    n := r.Read(b) handle (err) {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    n := r.Read(b)
}

سيكون لهذا النهج ميزة السماح للكود الحالي بالعمل ، مع تمكين آلية جديدة لمعالجة الأخطاء بشكل أفضل (على الأقل في بناء الجملة).

تعليقات على الكلمات الرئيسية:

  • قد لا يكون fails هو الخيار الأفضل ولكنه أفضل ما يمكنني التفكير فيه حاليًا. فكرت في استخدام err (أو errs ) ، لكن كيفية استخدامها حاليًا قد تجعل ذلك اختيارًا سيئًا بسبب التوقعات الحالية ( err هو على الأرجح اسم متغير ، و errs شريحة أو مصفوفة أو أخطاء).
  • handle مضللاً قليلاً. كنت أرغب في استخدام recover ، لكنه يُستخدم مقابل panic s ...

تحرير: تم تغيير r. قراءة الاستدعاء لمطابقة io.Reader.Read() .

يرجع جزء من سبب هذا الاقتراح إلى أن الأسلوب الحالي في Go لا يساعد الأدوات في فهم ما إذا كانت القيمة المرتجعة error تشير إلى فشل إحدى الوظائف أو إرجاع قيمة خطأ كجزء من وظيفتها (على سبيل المثال ، github.com/pkg/errors ).

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

ibrasho ، كيف

func (c EmailClient) SendEmail(to, content string) error {
    // ...

    // You can handle it and execution will continue if you don't fail or return
    _, _, err := r.Read()
        if err != nil {
        fmt.Printf("failed to read: %s", err)
    }

    // This shouldn't compile and should complain about an unhandled error
    _, _, err := r.Read()
}

... إذا قدمنا ​​تحذيرات للمترجم أو أعطيت بعض المعلومات للحالات غير المعالجة من error ؟ لا حاجة لتغيير اللغة.

شيئان:

  • أعتقد أن بناء الجملة المقترح يقرأ ويبدو أفضل. 😁
  • تتطلب روايتي إعطاء الوظائف القدرة على توضيح فشلها بشكل صريح. هذا شيء مفقود في Go حاليًا يمكنه تمكين الأدوات من فعل المزيد. يمكننا دائمًا التعامل مع دالة تُرجع قيمة error أنها فاشلة ، لكن هذا افتراض. ماذا لو أرجعت الدالة قيمتين error ؟

احتوى اقتراحي على شيء أزلته ، وهو نشر تلقائي:

func (c EmailClient) SendEmail(to, content string) fails {
    n := r.Read(b)

    // Would automaticaly propgate the error, so it will be equivlent to this:
    // n := r.Read(b) handle (err) {
    //  fail err
    // }
}

لقد أزلت ذلك لأنني أعتقد أن معالجة الأخطاء يجب أن تكون صريحة.

تحرير: تم تغيير r. قراءة الاستدعاء لمطابقة io.Reader.Read() .

إذن ، سيكون هذا توقيعًا صالحًا أو نموذجًا أوليًا؟

func (r *MyFileReader) Read(b []byte) (n int, err error) fails

(بالنظر إلى أن تنفيذ io.Reader يعين io.EOF عندما لا يكون هناك المزيد للقراءة وبعض الأخطاء الأخرى لظروف الفشل.)

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

كنت أقترح أن الفشل سيؤدي إلى إرجاع القيم كقيم صفرية. القارئ الحالي: اقرأ بالفعل قدم بعض الوعود التي قد لا تكون ممكنة مع هذا النهج الجديد.

عندما تواجه القراءة خطأً أو حالة نهاية الملف بعد قراءة n> 0 بايت بنجاح ، فإنها تُرجع عدد البايتات التي تمت قراءتها. قد تقوم بإرجاع الخطأ (non-nil) من نفس المكالمة أو إرجاع الخطأ (و n == 0) من مكالمة لاحقة. مثال لهذه الحالة العامة هو أن القارئ الذي يقوم بإرجاع عدد غير صفري من البايت في نهاية دفق الإدخال قد يُرجع إما err == EOF أو err == nil. يجب أن ترجع القراءة التالية 0 ، EOF.

يجب على المتصلين دائمًا معالجة n> 0 بايت التي تم إرجاعها قبل التفكير في الخطأ. يؤدي القيام بذلك بشكل صحيح إلى معالجة أخطاء الإدخال / الإخراج التي تحدث بعد قراءة بعض البايت وأيضًا سلوكيات EOF المسموح بها.

لا يتم تشجيع تطبيقات القراءة على إرجاع عدد البايتات الصفرية مع وجود خطأ صفري ، إلا إذا كان len (p) == 0. على وجه الخصوص لا يشير إلى EOF.

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

بشكل عام ، كيف يجب أن تتصرف الدالة عندما يتم إجراؤها جزئيًا عند فشلها؟ بصراحة لم أفكر في هذا حتى الآن.

حالة io.EOF بسيطة:

func DoSomething(r io.Reader) fails {
    // I'm using rerr so that I don't shadow the err returned from the function
    n, err := r.Read(b) handle (rerr) {
        if rerr != io.EOF {
            fail err
        }
        // Else do nothing?
    }
}

thejerf ، أشعر أن هناك الكثير من السلوك الغريب هنا. أنت تتصل بـ net.Listen ، والتي تُرجع خطأً ، لكن لم يتم تعيينها.

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

هل يتجاوز كل مؤجل جديد الأخير ، بحيث لا يتم استدعاء هذا الخطأ التوضيحي مطلقًا؟ أم أنها تتكدس ، بحيث إذا تم إرجاع خطأ من ، على سبيل المثال ، toServer.Send ، فحينئذٍ يتم استدعاء closeOnErr مرتين ، ثم يتم استدعاء annotateError؟

إنه يعمل كما هو مؤجل الآن: https://play.golang.org/p/F0xgP4h5Vxf كنت أتوقع بعض الإزعاج لهذا المنشور ، والذي كان ردي المخطط له هو الإشارة إلى أن هذا _ بالفعل _ كيف يعمل التأجيل وأنت فقط قم بإحباط سلوك Go الحالي ، لكنه لم يحصل على أي شيء. واحسرتاه. كما يظهر هذا المقتطف أيضًا ، فإن التظليل ليس مشكلة ، أو على الأقل ليس مشكلة أكثر مما هي عليه بالفعل. (لن يؤدي ذلك إلى إصلاحه أو جعله أسوأ بشكل خاص).

أعتقد أن أحد الجوانب التي قد تكون مربكة هو أنه بالفعل في Go الحالي أن المعلمة المسماة سينتهي بها الأمر "كل ما يتم إرجاعه بالفعل" ، لذا يمكنك فعل ما فعلته وأخذ مؤشر إليه وتمريره هذا إلى دالة مؤجلة ومعالجتها ، بغض النظر عما إذا كنت تقوم بإرجاع قيمة مثل return errors.New(...) ، والتي قد تبدو بديهيًا مثل "متغير جديد" ليس المتغير المحدد ، ولكن في الواقع ستنتهي Go مع تعيينه للمتغير المسمى بحلول وقت تشغيل المؤجل. من السهل تجاهل هذه التفاصيل الخاصة بـ Go الحالي الآن. أؤكد أنه على الرغم من أن الأمر قد يكون محيرًا الآن ، إلا أنك إذا عملت حتى على قاعدة بيانات تستخدم هذا المصطلح (على سبيل المثال ، لا أقول إن هذا يجب أن يصبح "ممارسة أفضل الممارسات" ، فقط عدد قليل من التعرضات ستفعل) د معرفة ذلك بسرعة كبيرة. لأنه ، فقط لأقولها مرة أخرى لأكون واضحًا جدًا ، هذه هي الطريقة التي يعمل بها Go بالفعل ، وليس تغييرًا مقترحًا.

هذا اقتراح أعتقد أنه لم يتم اقتراحه من قبل. باستخدام مثال:

 r, !handleError := something()

معنى هذا هو نفسه:

 r, _xyzzy := something()
 if ok, R := handleError(_xyzzy); !ok { return R }

(حيث _xyzzy هو متغير جديد يمتد نطاقه فقط إلى هذين السطرين من التعليمات البرمجية ، وقد يكون R قيمًا متعددة).

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

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

إليك كيف يمكنك استخدامه:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

أو يمكنك استخدامه في اختبار لاستدعاء t.Fatal إذا فشل كود init الخاص بك:

func TestSomething(t *testing.T) {
  must := func(err error) bool { t.Fatalf("init code failed: %s", err); return true }
  !must := setupTest()
  !must := clearDatabase()
  ...
}

أود أن أقترح تغيير توقيع الوظيفة إلى func(error) error . إنه يبسط حالة الأغلبية ، وإذا كنت بحاجة إلى مزيد من تحليل الخطأ ، فما عليك سوى استخدام الآلية الحالية.

السؤال النحوي: هل يمكنك تحديد الوظيفة المضمنة؟

func Read(filename string) error {
    f, !func(err error) error {
        if err != nil { return true, fmt.Errorf("... %s", err) }
        return false, nil
    } := OpenFile(filename)
    /...

أنا مرتاح لـ "لا تفعل ذلك" ، ولكن من المحتمل أن تسمح البنية لها بتقليل عدد الحالات الخاصة. سيسمح هذا أيضًا بما يلي:

func failed(s string) func(error) error {
    return func(err error) {
       // returns a decorated error with the given string
   }
}

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

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

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

thejerf لطيف ولكن مسار سعيد تم تغييره بشكل كبير.
منذ عدة رسائل ، كان هناك اقتراح بأن يكون لديك نوع Ruby مثل "أو" sintax - f := OpenFile(filename) or failed("couldn't open file") .

قلق إضافي - هل هذا لأي نوع من المعلمات أم للأخطاء فقط؟ إذا كان للأخطاء فقط - فيجب أن يكون للكتابة معنى خاص للمترجم.

thejerf لطيف ولكن مسار سعيد تم تغييره بشكل كبير.

أوصي بالتمييز بين المسار المشترك المحتمل للمقترح الأصلي حيث يبدو مثل اقتراح بولهانكين الأصلي:

func Read(filename string) error {
  herr := func(err error) (bool, error) {
      if err != nil { return true, fmt.Errorf("Read failed: %s", err) }
      return false, nil
  }

  f, !herr := OpenFile(filename)
  b, !herr := ReadBytes(f)
  !herr := ProcessBytes(b)
  return nil
}

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

إذا كنت ستخبرني أن "الطريق السعيد"

func Read(filename string) error {
  f, !failed("couldn't open file") := OpenFile(filename)
  b, !failed("couldn't read file") := ReadBytes(f)
  !failed("couldn't process file") := ProcessBytes(b)
  return nil
}

معقد ويصعب قراءته ، ولكن من السهل قراءة المسار السعيد

func Read(filename string) error {
  f, err := OpenFile(filename)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  b, err := ReadBytes(f)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  err = ProcessBytes(b)
  if err != nil {
    return fmt.Errorf("Read failed: %s", err)
  }

  return nil
}

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

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

فتح ملف.

ثم اقرأ بعض البايتات ، إذا كان ذلك جيدًا.

ثم قم بمعالجتها ، إذا كان هذا جيدًا.

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

لا تعجبني فكرة إخفاء بيان الإرجاع ، أفضل:

f := OpenFile(filename) or return failed("couldn't open file")
....
func failed(msg string, err error) error { ... } 

في هذه الحالة ، or هو عامل إعادة توجيه صفري مشروط ،
إعادة توجيه آخر عائد إذا كان غير صفري.
هناك اقتراح مشابه في C # باستخدام عامل التشغيل ?>

f := OpenFile(filename) ?> return failed("couldn't open file")

thejerf "happy path" في قضيتك مُسبق باستدعاء فشل (...) ، والذي قد يكون طويلًا جدًا. يبدو أيضًا مثل yoda: rofl:

أحرف rodcorsi التي تم إدخالها باستخدام مفتاح "shift" هي أحرف IMHO سيئة - (إذا كان لديك تخطيطات متعددة للوحة المفاتيح)

من فضلك لا تجعل هذه الطريقة أكثر تعقيدًا مما هي عليه الآن. إن نقل نفس الرموز في سطر واحد (بدلاً من 3 أو أكثر) ليس حلاً حقًا. أنا شخصيا لا أرى أي من هذه المقترحات قابلة للتطبيق إلى هذا الحد. يا رفاق ، الرياضيات بسيطة للغاية. إما أن تتبنى فكرة "Try-catch" أو احتفظ بالأشياء على ما هي عليه الآن مما يعني الكثير من "if then else" وضوضاء التعليمات البرمجية وليس مناسبًا حقًا للاستخدام في أنماط OO مثل Fluent Interface.

شكرًا جزيلاً على كل ما تبذلونه من المساعدة وربما القليل من اليد ؛-) (فقط أمزح)

KamyarM IMO ، "استخدام أفضل بديل معروف أو عدم إجراء أي تغييرات على الإطلاق" ليس بيانًا مثمرًا للغاية. يوقف الابتكار ويسهل الحجج الدائرية.

KernelDeimos أنا أتفق معك ولكني أرى العديد من التعليقات على هذا الموضوع الذي كان يؤيد بشكل أساسي القديم بنقل 4 5 أسطر بالضبط في سطر واحد لا أراه حلاً حقًا وأيضًا يرفض العديد في Go Community استخدامًا موثوقًا جدًا حاول الالتقاط الذي يغلق الأبواب أمام أي آراء أخرى. أنا شخصياً أعتقد أن أولئك الذين اخترعوا مفهوم try-catch فكروا به حقًا ، وعلى الرغم من أنه قد يكون به بعض العيوب ، إلا أن هذه العيوب ناتجة فقط عن عادات البرمجة السيئة ولا توجد طريقة لإجبار المبرمجين على كتابة أكواد جيدة حتى إذا قمت بإزالة أو تقييد قد يقول كل الخير أو البعض ميزات سيئة قد تحتويها لغة البرمجة.
لقد اقترحت شيئًا كهذا من قبل وهذا ليس بالضبط جافا أو C # try-catch وأعتقد أنه يمكن أن يدعم معالجة الأخطاء والمكتبات الحالية وأستخدم أحد الأمثلة المذكورة أعلاه. لذلك يقوم المترجم بالتحقق من الأخطاء بعد كل سطر والانتقال إلى كتلة catch إذا تم ضبط قيمة err على nil:

try (var err error){ 
    f, err := OpenFile(filename)
    b, err := ReadBytes(f)
    err = ProcessBytes(b)
    return nil
} catch (err error){ //Required
   return err
} finally{ // Optional
    // Do something else like close the file or connection to DB. Not necessary in this example since we  return earlier.
}

تضمين التغريدة
في مثالك ، كيف أعرف (وقت كتابة الكود) الطريقة التي أعادت الخطأ؟ كيف يمكنني تنفيذ الطريقة الثالثة لمعالجة الخطأ ("إرجاع الخطأ بمعلومات سياقية إضافية")؟

urandom
إحدى الطرق هي استخدام مفتاح Go والعثور على نوع الاستثناء في الصيد. لنفترض أن لديك استثناء pathError تعرفه ناتجًا عن OpenFile () بهذه الطريقة. هناك طريقة أخرى لا تختلف كثيرًا عن الحالية إذا أخطأت! = لا يوجد معالجة للخطأ في GoLang وهي:

try (var err error){ 
    f, err := OpenFile(filename)
} catch (err error){ //Required
   return err
}
try (var err error){ 
    b, err := ReadBytes(f)
catch (err error){ //Required
   return err
}
try (var err error){ 
    err = ProcessBytes(b)
catch (err error){ //Required
   return err
}
return nil

لذلك لديك خيارات بهذه الطريقة ولكنك غير محدود. إذا كنت تريد حقًا معرفة السطر الذي تسبب في المشكلة بالضبط ، فأنت تضع كل سطر في محاولة الالتقاط بنفس الطريقة التي تكتب بها الآن الكثير من if-then-elses. إذا لم يكن الخطأ مهمًا بالنسبة لك وتريد تمريره إلى طريقة المتصل التي في الأمثلة التي تمت مناقشتها في هذا الموضوع تتعلق في الواقع بأني أعتقد أن الكود الذي أقترحه يؤدي المهمة فقط.

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

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

المشكلة الأساسية ، كما أراها ، هي أن رمز معالجة الأخطاء في وظيفة ما يتكرر إذا كان جزء من المنطق هو "العودة إلى المتصل". إذا كنت تريد تغيير المنطق في أي وقت ، فعليك تغيير كل مثيل if err != nil { return nil } .

ومع ذلك ، فأنا معجب حقًا بفكرة try...catch طالما أن الوظائف لا يمكنها throw أي شيء ضمنيًا.

شيء آخر أعتقد أنه سيكون مفيدًا إذا كان المنطق في catch {} يتطلب كلمة رئيسية break لكسر تدفق التحكم. تريد أحيانًا معالجة خطأ ما دون كسر تدفق التحكم. (على سبيل المثال: "لكل عنصر من هذه البيانات ، افعل شيئًا ، أضف خطأ غير معدوم إلى القائمة وتابع")

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

أفضل مزج "أو إرجاع" مع # 19642 ، # 21498 بدلاً من استخدام try..catch (التأجيل / الذعر / الاسترداد موجود بالفعل ؛ الرمي في نفس الوظيفة يشبه وجود العديد من عبارات goto وأصبح فوضويًا مع تبديل نوع إضافي داخل catch ؛ يسمح بنسيان معالجة الأخطاء عن طريق المحاولة .. الالتقاط عاليًا في المكدس (أو يعقد المحول البرمجي بشكل كبير إذا حاول النطاق .. الالتقاط داخل دالة واحدة)

تضمين التغريدة
يبدو أن صيغة try- catchKamyarM تشير إلى بعض بناء الجملة للتعامل مع متغيرات إرجاع الخطأ ، وليس مقدمة للاستثناءات. على الرغم من أنني أفضل بناء جملة نوع "أو إرجاع" لأسباب مختلفة ، إلا أنه يبدو اقتراحًا شرعيًا.

ومع ذلك ، KamyarM ، لماذا يحتوي try على جزء تعريف متغير فيه؟ أنت تحدد متغير err ، لكن المتغيرات الأخرى err الموجودة في الكتلة نفسها تحجبها. ما هو الغرض منه؟

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

egorse بالضبط ما ذكره DeedleFake هناك الغرض منه. هذا يعني أن كتلة try لها عين على هذا الكائن. كما أنه يحد من نطاقه. إنه شيء مشابه لبيان الاستخدام في C #. في C # ، يتم التخلص من الكائن (الكائنات) التي تم تحديدها باستخدام الكلمة الأساسية تلقائيًا بمجرد تنفيذ تلك الكتلة ويقتصر نطاق تلك الكائنات على كتلة "استخدام".
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement

يعد استخدام المصيد ضروريًا لأننا نريد إجبار المبرمج على تحديد كيفية التعامل مع الخطأ بطريقة صحيحة. في C # و Java ، يعد المصيد إلزاميًا أيضًا. في C # إذا كنت لا تريد التعامل مع استثناء ، فلا تستخدم try-catch في هذه الوظيفة على الإطلاق. عندما يحدث استثناء ، يمكن لأي طريقة في التسلسل الهرمي للمكالمات معالجة الاستثناء أو حتى إعادة طرحه (أو لفه في استثناء آخر) مرة أخرى. لا أعتقد أنه يمكنك فعل الشيء نفسه في Java. في Java ، الطريقة التي قد تطرح استثناء تحتاج إلى التصريح عنه في توقيع الوظيفة.

أريد أن أؤكد أن كتلة try-catch هذه ليست هي نفسها. لقد استخدمت هذه الكلمات الرئيسية لأن هذا يشبه ما تريد تحقيقه وأيضًا هذا ما يعرفه العديد من المبرمجين ويتم تدريسهم في معظم دورات مفاهيم البرمجة.

يمكن أن يكون هناك _return on error_ مهمة ، والتي تعمل فقط في حالة وجود معلمة إرجاع خطأ مسمى _ ​​، كما في:

func process(someInput string) (someOutput string, err error) {
    err ?= otherAction()
    return
}

إذا لم يكن err ليس nil فقم بالعودة.

أعتقد أن هذه المناقشة حول إضافة السكر try إلى Rust ستكون مضيئة للمشاركين في هذه المناقشة.

FWIW ، فكرة قديمة حول تبسيط معالجة الأخطاء (نعتذر إذا كان هذا هراء):

يمكن استخدام معرّف الارتفاع ، الذي يُشار إليه برمز علامة الإقحام ^ ، كأحد المعاملات على الجانب الأيسر من المهمة. لأغراض التخصيص ، فإن معرّف الزيادة هو اسم مستعار لقيمة الإرجاع الأخيرة للدالة المحتوية ، سواء كان للقيمة اسم أم لا. بعد اكتمال التخصيص ، تختبر الوظيفة القيمة المرجعة الأخيرة مقابل القيمة الصفرية لنوعها (لا شيء ، 0 ، خطأ ، ""). إذا تم اعتبارها صفرًا ، فستستمر الوظيفة في التنفيذ ، وإلا فإنها ستعود.

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

كمثال ، ضع في اعتبارك الكود التالي:

func Alpha() (string, error) {

    b, ^ := beta()
    g, ^ := gamma()
    return b + g, nil
}

هذا يعادل تقريبًا:

func Alpha() (ret1 string, ret2 error) {

    b, ret2 := beta()
    if ret2 != nil {
        return
    }

    g, ret2 := gamma()
    if ret2 != nil {
        return
    }

    return b + g, nil
}

يكون البرنامج مشوهًا إذا:

  • يتم استخدام معرف الارتفاع أكثر من مرة في مهمة
  • لا تقوم الدالة بإرجاع قيمة
  • لا يحتوي نوع قيمة الإرجاع الأخيرة على اختبار ذي مغزى وفعال للصفر

هذا الاقتراح مشابه للآخرين من حيث أنه لا يعالج مشكلة توفير معلومات سياقية أكبر ، لما يستحق.

gboyle لهذا السبب يجب تسمية آخر قيمة إرجاع IMO ومن النوع error . هذا وقد اثنين من آثار هامة:

1 - يتم تسمية قيم الإرجاع الأخرى أيضًا ، وبالتالي
2 - لديهم بالفعل قيم صفرية ذات مغزى.

@ object88 كما يوضح لنا تاريخ الحزمة context ، هذا يحتاج إلى بعض الإجراءات من الفريق الأساسي ، مثل تحديد نوع error مضمّن (مجرد الانتقال العادي error ) مع بعض السمات المشتركة (message؟ call stack؟ إلخ).

AFAIK ليس هناك الكثير من تركيبات اللغة السياقية في Go. إلى جانب go و defer لا توجد عناصر أخرى وحتى هذين هما واضحان وواضحان للغاية (الروبوت في التركيب - وللعين - والدلالات).

ماذا عن شيء كهذا؟

(نسخ رمز حقيقي أعمل عليه):

func (g *Generator) GenerateDevices(w io.Writer) error {
    var err error
    catch err {
        _, err = io.WriteString(w, "package cc\n\nconst (") // if err != nil { goto Caught }
        for _, bd := range g.zwClasses.BasicDevices {
            _, err = w.Write([]byte{'\t'}) // if err != nil { goto Caught }
            _, err = io.WriteString(w, toGoName(bd.Name)) // if err != nil { goto Caught }
            _, err = io.WriteString(w, " BasicDeviceType = ") // if err != nil { goto Caught }
            _, err = io.WriteString(w, bd.Key) // if err != nil { goto Caught }
            _, err = w.Write([]byte{'\n'}) // if err != nil { goto Caught }
        }
        _, err = io.WriteString(w, ")\n\nvar BasicDeviceTypeNames = map[BasicDeviceType]string{\n") // if err != nil { goto Caught }
       // ...snip
    }
    // Caught:
    return err
}

عندما يكون err غير صفري ، فإنه ينقسم إلى نهاية عبارة "catch". يمكنك استخدام "catch" لتجميع الاستدعاءات المتشابهة معًا والتي عادةً ما تُرجع نفس نوع الخطأ. حتى لو لم تكن المكالمات مرتبطة ، يمكنك التحقق من أنواع الأخطاء بعد ذلك ولفها بشكل مناسب.

lukescott قرأت منشور المدونة هذا بواسطة https://blog.golang.org/errors-are-values

davecheney تحافظ فكرة الصيد (بدون المحاولة) على روح هذا الشعور. يعامل الخطأ كقيمة. إنه ينكسر ببساطة (داخل نفس الوظيفة) عندما لا تكون القيمة صفرية. لا يؤدي إلى تعطل البرنامج بأي شكل من الأشكال.

lukescott يمكنك استخدام تقنية Rob اليوم ، ولست بحاجة إلى تغيير اللغة.

هناك فرق كبير بين الاستثناءات والأخطاء:

  • من المتوقع حدوث أخطاء (يمكننا كتابة اختبار لهم) ،
  • الاستثناءات غير متوقعة (ومن هنا جاء "الاستثناء") ،

تعامل العديد من اللغات على حد سواء كاستثناء.

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

أود البناء على الاقتراح المقدم من thejerf قليلاً.

أولاً ، بدلاً من ! ، تم تقديم عامل تشغيل or ، وهذا shift هو آخر وسيطة تم إرجاعها من استدعاء الوظيفة على الجانب الأيسر ، ويستدعي تعليمة return على اليمين ، والذي يكون تعبيره هو دالة تسمى ، إذا كانت الوسيطة shifted ليست صفرية (غير صفرية لأنواع الخطأ) ، فمررها تلك الوسيطة. إنه جيد إذا اعتقد الناس أنه يجب أن يكون فقط لأنواع الأخطاء أيضًا ، على الرغم من أنني أشعر أن هذا البناء سيكون مفيدًا للوظائف التي ترجع قيمة منطقية كحجة أخيرة أيضًا (شيء جيد / ليس جيدًا).

ستبدو طريقة القراءة كما يلي:

func Read(filename string) error {
  f := OpenFile(filename) or return errors.Contextf("opening file %s", filename)
  b := ReadBytes(f) or return errors.Contextf("reading file %s", filename)
  ProcessBytes(b) or return errors.Context("processing data")
  return nil
}

أفترض أن حزمة الأخطاء توفر وظائف ملائمة مثل ما يلي:

func Noop() func(error) error {
   return func(err error) {
       return err   
   }
}


func Context(msg string) func(error) error {
    return func(err error) {
        return fmt.Errorf("%s: %v", msg, err)
    }
}
...

يبدو هذا مقروءًا تمامًا ، بينما يغطي جميع النقاط الضرورية ، ولا يبدو غريبًا جدًا أيضًا ، بسبب الإلمام ببيان الإرجاع.

urandom في هذا البيان f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) كيف يمكن معرفة السبب؟ على سبيل المثال ، هل هو عدم وجود إذن قراءة أم أن الملف غير موجود على الإطلاق؟

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

urandom IMO يخفي الكثير.

2 سنتي هنا ، أود أن أقترح قاعدة بسيطة:

"معلمة خطأ النتيجة الضمنية للوظائف"

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

افترض أن لدينا وظيفة محددة على النحو التالي من أجل المناقشة:

func f () (int) {}
وهو مطابق لـ: func f () (int، error) {}
وفقًا لقاعدة خطأ النتيجة الضمنية.

للتعيين ، يمكنك إظهار الخطأ أو تجاهله أو اكتشافه على النحو التالي:

1) فقاعة

س: = و ()

إذا أعادت f الخطأ ، فستعود الوظيفة الحالية على الفور مع الخطأ
(أو إنشاء حزمة أخطاء جديدة؟)
إذا كانت الوظيفة الحالية رئيسية ، فسيتوقف البرنامج.

إنه مكافئ لمقتطف الشفرة التالي:

x ، يخطئ: = f ()
إذا أخطأت! = لا شيء {
العودة ... ، يخطئ
}

2) تجاهل

س ، _: = و ()

معرف فارغ في نهاية قائمة تعبير التخصيص للإشارة صراحة إلى نبذ الخطأ.

3) قبض

x ، يخطئ: = f ()

يجب التعامل مع يخطئ كالمعتاد.

أعتقد أن هذا التغيير الاصطلاحي للشفرة يجب أن يتطلب فقط تغييرات طفيفة في المترجم
أو المعالج يجب أن يقوم بهذه المهمة.

@ dc0d هل يمكنك إعطاء مثال على ما

urandom هو السبب الذي تسبب في طرح السؤال "أين الخطأ الأصلي؟" كما سألته في تعليق سابق. تمرر الخطأ ضمنيًا وليس من الواضح (على سبيل المثال) مكان الخطأ الأصلي في هذا السطر: f := OpenFile(filename) or return errors.Contextf("opening file %s", filename) . الخطأ الأصلي الذي تم إرجاعه بواسطة OpenFile() - والذي قد يكون شيئًا مثل عدم وجود إذن قراءة أو غياب الملف وليس فقط "هناك خطأ في اسم الملف".

@ dc0d
أنا أعترض. إنه أمر واضح مثل التعامل مع المتعاملين مع http ، حيث تمررهم لاحقًا إلى بعض المسك ، وفجأة تحصل على طلب وكاتب استجابة. والناس معتادون على هذا النوع من السلوك. كيف يعرف الناس ما يفعله كشف الحساب go ؟ من الواضح أنه ليس واضحًا في اللقاء الأول ، ومع ذلك فهو واسع الانتشار وموجود في اللغة.

لا أعتقد أننا يجب أن نعارض أي اقتراح على أساس أنه جديد ولا أحد لديه أي فكرة عن كيفية عمله ، لأن هذا صحيح بالنسبة لمعظمهم.

@ عشوائي الآن هذا أكثر منطقية (بما في ذلك http.Handler مثال).

ونحن نناقش الأشياء. أنا لا أتحدث ضد أو مع أي فكرة محددة. لكنني أؤيد البساطة والصراحة وفي نفس الوقت أنقل القليل من العقلانية حول تجربة المطور.

@ dc0d

والذي قد يكون شيئًا مثل عدم وجود إذن قراءة أو غياب الملف

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

creker أخطاء ليست استثناءات (بعض التعليقات السابقة لي). الأخطاء عبارة عن قيم ، لذا فإن رميها أو إعادة طرحها غير ممكن. للتجربة / السيناريوهات الشبيهة بالقبض على Go لديه الذعر / الاسترداد.

@ dc0d أنا لا أتحدث عن الاستثناءات. أعني بإعادة الرمي إعادة الخطأ إلى المتصل. or return errors.Contextf("opening file %s", filename) المقترح يلف ويعيد طرح خطأ.

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

@ dc0d هذه تفاصيل تنفيذية وقد تتغير في المستقبل. ويمكن أن يتغير في الواقع ، وقائية غير تعاونية قيد العمل الآن.

تضمين التغريدة
أعتقد أنه يمكنك تغطية حالات أكثر من مجرد إرجاع خطأ معدل:

func retryReadErrHandler(filename string, count int) func(error) error {
     return func(err error) error {
          if os.IsTimeout(err) {
               count++
               return Read(filename, count)
          }
          if os.IsPermission(err) {
               log.Fatal("Permission")
          }

          return fmt.Errorf("opening file %s: %v", filename, err)
      }
}

func Read(filename string, count int) error {
  if count > 3 {
    return errors.New("max retries")
  }

  f := OpenFile(filename) or return retryReadErrHandler(filename, count)

  ...
}

@ dc0d
من المحتمل أن يتم تضمين استدعاءات الوظائف الإضافية بواسطة المترجم

@ العشوائية التي تبدو مثيرة جدا للاهتمام. سحري بعض الشيء مع حجة ضمنية ولكن يمكن أن تكون في الواقع عامة وموجزة بما يكفي لتغطية كل شيء. فقط في حالات نادرة جدًا ، قد تضطر إلى اللجوء إلى if err != nil العادي

urandom ، أنا retryReadErrHandler بإرجاع func؟

@ object88
هذه هي الفكرة وراء عامل التشغيل or return . تتوقع وظيفة سوف تستدعيها في حالة وجود وسيطة غير صفرية تم إرجاعها من الجانب الأيسر. في هذا الصدد ، يعمل تمامًا مثل http.Handler ، تاركًا المنطق الفعلي لكيفية التعامل مع الوسيطة وإعادتها (أو الطلب واستجابته ، في حالة المعالج) ، إلى رد الاتصال. ومن أجل استخدام البيانات المخصصة الخاصة بك في رد الاتصال ، يمكنك إنشاء وظيفة مجمعة تتلقى تلك البيانات كمعلمات وإرجاع ما هو متوقع.

أو بعبارات أكثر شيوعًا ، فهو مشابه لما نفعله عادةً مع المتعاملين:
"اذهب
func nodesHandler (repo Repo) http.Handler {
إرجاع http.HandlerFunc (func (w http.ResponseWriter، r * http.Request) {
data، _: = json.Marshal (repo.GetNodes ())
ث.الكتابة (البيانات)
})
}

urandom ، يمكنك تجنب بعض السحر من خلال ترك LHS كما هو الحال اليوم وتغيير or ... return إلى returnif (cond) :

func Read(filename string) error {
   f, err := OpenFile(filename) returnif(err != nil) errors.Contextf(err, "opening file %s", filename)
   b, err := ReadBytes(f) returnif(err != nil) errors.Contextf(err, "reading file %s", filename)
   err = ProcessBytes(b) returnif(err != nil) errors.Context(err, "processing data")
   return nil
}

يؤدي ذلك إلى تحسين التعميم والشفافية لقيم الخطأ على اليسار وشرط التشغيل على اليمين.

كلما رأيت هذه المقترحات المختلفة ، كلما كنت أميل إلى تغيير حكومي فقط. اللغة لديها بالفعل القوة ، دعونا نجعلها أكثر قابلية للفحص. billyh ، ليس لاختيار اقتراحك على وجه الخصوص ولكن returnif(cond) ... هو مجرد طريقة لإعادة كتابة if cond { return ...} . لماذا لا نكتب الأخير فقط؟ نحن نعلم بالفعل ماذا يعني ذلك.

x, err := foo()
if err != nil { return fmt.Errorf(..., err) }

او حتى

if x, err := foo(); err != nil { return fmt.Errorf(..., err) }

أو

x, err := foo(); if err != nil { return fmt.Errorf(..., err) }

لا توجد كلمات رئيسية سحرية أو بناء جملة أو عوامل تشغيل.

(قد يكون من المفيد إذا قمنا أيضًا بإصلاح # 377 لإضافة بعض المرونة لاستخدام := .)

@ randall77 أنا أميل بشكل متزايد إلى هذه الطريقة أيضًا.

@ randall77 في أي نقطة سيتم تغليف الخط؟

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

as ، لم أفكر في الأمر تمامًا ، ولكن ربما "إذا كان نص بيان if يحتوي على كشف حساب return ، فسيتم تنسيق بيان if هذا النحو سطر واحد ".

ربما يجب أن يكون هناك قيد إضافي على الشرط if ، مثل أنه يجب أن يكون متغيرًا منطقيًا أو عامل تشغيل ثنائي من متغيرين أو ثوابت.

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

@ randall77
ما تقترحه هو أكثر انسجاما كاقتراح نمط الكود ، وهذا هو المكان الذي تذهب إليه بشدة. قد لا تعمل بشكل جيد في المجتمع ككل لأنه يوجد فجأة نمطين من تنسيق عبارات if.

ناهيك عن أن إحدى الخطوط العريضة مثل تلك يصعب قراءتها. if != ; { } أكثر من اللازم حتى في عدة أسطر ، ومن هنا جاء هذا الاقتراح. تم إصلاح النمط لجميع الحالات تقريبًا ويمكن تحويله إلى سكر نحوي يسهل قراءته وفهمه.

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

أنا أحب المشاعر "الأخطاء هي القيم" الخاصة بـ Go ، لكنني أوافق أيضًا على أن if err := expression; err != nil { return err } مطول جدًا ، ويرجع ذلك أساسًا إلى أنه من المتوقع أن تؤدي كل مكالمة تقريبًا إلى إرجاع خطأ. هذا يعني أنك ستحصل على الكثير من هذه الأشياء ، ومن السهل إفسادها ، اعتمادًا على مكان الإعلان عن الخطأ (أو التظليل). لقد حدث ذلك مع رمزنا.

نظرًا لأن Go لا يستخدم try / catch ويستخدم الذعر / التأجيل لظروف "استثنائية" ، فقد تكون لدينا فرصة لإعادة استخدام المحاولة و / أو التقاط الكلمات الرئيسية لتقصير معالجة الأخطاء دون تعطل البرنامج.

هذه فكرة راودتني:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
}

الفكرة هي أنك تسبق err على LHS بالكلمة الأساسية try . إذا كان الخطأ ليس لا شيء ، فسيتم إرجاعه على الفور. لا يتعين عليك استخدام الصيد هنا ، ما لم يكن العائد غير راضٍ تمامًا. يتوافق هذا بشكل أكبر مع توقعات الأشخاص من "محاولة تنفيذ الفواصل" ، ولكن بدلاً من تعطل البرنامج ، فإنه يعود فقط.

إذا لم يكن الإرجاع مُرضيًا تمامًا (فحص وقت الترجمة) أو كنا نريد إنهاء الخطأ ، فيمكننا استخدام catch كتسمية خاصة للأمام فقط مثل هذا:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, try err = io.WriteString(w, "bar")
    return
catch:
    return &CustomError{"some detail", err}
}

يتيح لك هذا أيضًا التحقق من بعض الأخطاء وتجاهلها:

func WriteFooBar(w io.Writer) (err error) {
    _, try err = io.WriteString(w, "foo")
    _, try err = w.Write([]byte{','})
    _, err = io.WriteString(w, "bar")
        if err == io.EOF {
            err = nil
        } else {
            goto catch
        }
    return
catch:
    return &CustomError{"some detail", err}
}

ربما يمكنك حتى الحصول على try حدد التصنيف:

func WriteFooBar(w io.Writer) (err error) {
    _, try(handle1) err = io.WriteString(w, "foo")
    _, try(handle2) err = w.Write([]byte{','})
    _, try(handle3) err = io.WriteString(w, "bar")
    return
handle1:
    return &CustomError1{"...", err}
handle2:
    return &CustomError2{"...", err}
handle3:
    return &CustomError3{"...", err}
}

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

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

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

يبدو نموذج كود thejerf بهذا lukescott :

func NewClient(...) (*Client, error) {
    listener, try err := net.Listen("tcp4", listenAddr)
    defer func() {
        if err != nil {
            listener.Close()
        }
    }()

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    defer func() {
        if err != nil {
            conn.Close()
        }
    }()

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    return nil, err
}

ينتقل من 59 سطرًا إلى 47.

هذا هو نفس الطول ، لكنني أعتقد أنه أكثر وضوحًا من استخدام defer :

func NewClient(...) (*Client, error) {
    var openedListener, openedConn bool
    listener, try err := net.Listen("tcp4", listenAddr)
    openedListener = true

    conn, try err := ConnectionManager{}.connect(server, tlsConfig)
    openedConn = true

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil

catch:
    if openedConn {
        conn.Close()
    }
    if openedListener {
        listener.Close()
    }
    return nil, err
}

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

بعد أن لعبت برمز المثال قليلاً ، فأنا الآن ضد المتغير try(label) name . أعتقد أنه إذا كان لديك العديد من الأشياء الفاخرة للقيام بها ، فما عليك سوى استخدام النظام الحالي if err != nil { ... } . إذا كنت تقوم بنفس الشيء بشكل أساسي ، مثل تعيين رسالة خطأ مخصصة ، فيمكنك القيام بذلك:

func WriteFooBar(w io.Writer) (err error) {
    msg := "thing1 went wrong"
    _, try err = io.WriteString(w, "foo")
    msg = "thing2 went wrong"
    _, try err = w.Write([]byte{','})
    msg = "thing3 went wrong"
    _, try err = io.WriteString(w, "bar")
    return nil

catch:
    return &CustomError{msg, err}
}

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

أحد الأشياء التي يمكن القيام بها هو جعل nil قيمة خاطئة ولتقييم القيم الأخرى على أنها صحيحة ، لذلك ينتهي بك الأمر بـ:

err := doSomething()
if err { return err }

لكنني لست متأكدًا من أن ذلك سينجح حقًا وسيختصر فقط بعض الشخصيات.
لقد أخطأت في العديد من الأشياء ، لكنني لا أعتقد أنني كتبت != nil الإطلاق.

لقد تم ذكر جعل الواجهات true / false من قبل ، وقلت إن ذلك سيجعل الأخطاء التي تحتوي على أعلام أكثر شيوعًا:

verbose := flag.Bool("v", false, "verbose logging")
flag.Parse()
if verbose { ... } // should be *verbose!

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

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    msg := fmt.Sprintf("While writing %s, thing1 went wrong", f.foo)
    _, try err = io.WriteString(w, f.foo)
    msg = fmt.Sprintf("While writing %s, thing2 went wrong", f.separator)
    _, try err = w.Write(f.separator)
    msg = fmt.Sprintf("While writing %s, thing3 went wrong", f.bar)
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    return &CustomError{msg, err}
}

@ object88 ، أعتقد أن تحليل SSA يجب أن يكون قادرًا على معرفة ما إذا لم يتم استخدام مهام معينة وإعادة ترتيبها بحيث لا تحدث إذا لم تكن هناك حاجة (هل أنت متفائل جدًا؟). إذا كان هذا صحيحًا ، فيجب أن يكون ذلك فعالاً:

func (f *foo) WriteFooBar(w io.Writer) (err error) {
    var format string, args []interface{}

    msg = "While writing %s, thing1 went wrong", 
    args = []interface{f.foo}
    _, try err = io.WriteString(w, f.foo)

    format = "While writing %s, thing2 went wrong"
    args = []interface{f.separator}
    _, try err = w.Write(f.separator)

    format = "While writing %s, thing3 went wrong"
    args = []interface{f.bar}
    _, try err = io.WriteString(w, f.bar)
    return nil

catch:
    msg := fmt.Sprintf(format, args...)
    return &CustomError{msg, err}
}

هل سيكون هذا قانوني؟

func Foo() error {
catch:
    try _ = doThing()
    return nil
}

أعتقد أن ذلك يجب أن يستمر حتى يعود doThing() لا شيء ، لكن يمكنني أن أقنع بخلاف ذلك.

تضمين التغريدة

بعد أن لعبت باستخدام رمز المثال قليلاً ، فأنا الآن ضد متغير اسم المحاولة (التسمية).

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

هل سيكون هذا قانوني؟

أود أن أقول نعم لأن المحاولة يجب أن تكون للأمام فقط. إذا أردت القيام بذلك ، فسأقول إنك بحاجة إلى القيام بذلك على النحو التالي:

func Foo() error {
tryAgain:
    if err := doThing(); err != nil {
        goto tryAgain
    }
    return nil
}

او مثل هذا:

func Foo() error {
    for doThing() != nil {}
    return nil
}

تضمين التغريدة

أحد الأشياء التي يمكن القيام بها هو جعل لا شيء قيمة خاطئة ولتقييم القيم الأخرى على أنها صحيحة ، لذلك ينتهي بك الأمر بـ: err := doSomething() if err { return err }

أعتقد أن هناك قيمة في تقصيرها. ومع ذلك ، لا أعتقد أنه يجب أن ينطبق على الصفر في جميع المواقف. ربما يمكن أن تكون هناك واجهة جديدة مثل هذه:

interface Truthy {
  True() bool
}

ثم يمكن استخدام أي قيمة تنفذ هذه الواجهة على النحو الذي اقترحته.

سيعمل هذا طالما أن الخطأ نفذ الواجهة:

err := doSomething()
if err { return err }

لكن هذا لن ينجح:

err := doSomething()
if err == true { return err } // err is not true

أنا جديد حقًا في golang ولكن ما رأيك في تقديم المفوض الشرطي كما هو موضح أدناه؟

func someFunc() error {

    errorHandler := delegator(arg1 Arg1, err error) error if err != nil {
        // ...
        return err // or modifiedErr
    }

    ret, err := doSomething()
    delegate errorHandler(ret, err)

    ret, err := doAnotherThing()
    delegate errorHandler(ret, err)

    return nil
}

المفوض هو وظيفة مثل الأشياء ولكن

  • يعني return return from its caller context . (يجب أن يكون نوع الإرجاع هو نفسه المتصل)
  • يتطلب الأمر اختياريًا if قبل { ، في المثال أعلاه هو if err != nil .
  • يجب أن يتم تفويضها من قبل المتصل بـ delegate كلمة رئيسية

قد يكون قادرًا على حذف delegate للتفويض ، لكنني أعتقد أنه يجعل من الصعب قراءة تدفق الوظيفة.

وربما يكون مفيدًا ليس فقط في معالجة الأخطاء ، لست متأكدًا الآن.

من الجميل إضافة شيك ، ولكن يمكنك القيام بالمزيد قبل العودة:

result, err := openFile(f);
if err != nil {
        log.Println(..., err)
    return 0, err 
}

يصبح

result, err := openFile(f);
check err

"اذهب
النتيجة ، يخطئ: = openFile (f) ؛
تحقق من الخطأ {
log.Println (... ، يخطئ)
}

```Go
reslt, _ := check openFile(f)
// If err is not nil direct return, does not execute the next step.

"اذهب
النتيجة ، يخطئ: = تحقق من الملف المفتوح (و) {
log.Println (... ، يخطئ)
}

It also attempts simplifying the error handling (#26712):
```Go
result, err := openFile(f);
check !err {
    // err is an interface with value nil or holds a nil pointer
    // it is unusable
    result.C...()
}

يحاول أيضًا تبسيط معالجة الخطأ (من قبل البعض الذي يعتبره مملًا) (# 21161). سيصبح:

result, err := openFile(f);
check err {
   // handle error and return
    log.Println(..., err)
}

بالطبع ، يمكنك استخدام try وكلمات رئيسية أخرى بدلاً من check ، إذا كان الأمر أكثر وضوحًا.

reslt, _ := try openFile(f)
// If err is not nil direct return, does not execute the next step.

"اذهب
النتيجة ، يخطئ: = openFile (f) ؛
حاول يخطئ {
// معالجة الخطأ والعودة
log.Println (... ، يخطئ)
}

Reference:

A plain idea, with support for error decoration, but requiring a more drastic language change (obviously not for go1.10) is the introduction of a new check keyword.

It would have two forms: check A and check A, B.

Both A and B need to be error. The second form would only be used when error-decorating; people that do not need or wish to decorate their errors will use the simpler form.

1st form (check A)
check A evaluates A. If nil, it does nothing. If not nil, check acts like a return {<zero>}*, A.

Examples

If a function just returns an error, it can be used inline with check, so
```Go
err := UpdateDB()    // signature: func UpdateDb() error
if err != nil {
    return err
}

يصبح

check UpdateDB()

بالنسبة لدالة ذات قيم إرجاع متعددة ، ستحتاج إلى تعيينها ، كما نفعل الآن.

a, b, err := Foo()    // signature: func Foo() (string, string, error)
if err != nil {
    return "", "", err
}

// use a and b

يصبح

a, b, err := Foo()
check err

// use a and b

النموذج الثاني (الاختيار أ ، ب)
تحقق A ، B تقيم A. إذا كانت لا شيء ، فلن تفعل شيئًا. إذا لم يكن لا شيء ، تحقق من التصرفات مثل الإرجاع {}*، ب.

هذا لاحتياجات تزيين الخطأ. ما زلنا نتحقق من A ، ولكن B هو المستخدم في الإرجاع الضمني.

مثال

a, err := Bar()    // signature: func Bar() (string, error)
if err != nil {
    return "", &BarError{"Bar", err}
}

يصبح

a, err := Foo()
check err, &BarError{"Bar", err}

ملحوظات
إنه خطأ تجميع لـ

استخدم بيان التحقق على الأشياء التي لا يتم تقييمها على أنها خطأ
استخدم التحقق في دالة ذات قيم إرجاع ليست في النموذج {type} * ، خطأ
التحقق من النموذج الثنائي A ، B قصير الدائرة. لا يتم تقييم "ب" إذا كانت "أ" لا شيء.

ملاحظات على التطبيق العملي
هناك دعم لأخطاء التزيين ، لكنك تدفع مقابل التحقق من الأخطاء A و B فقط عندما تحتاج بالفعل إلى تزيين الأخطاء.

بالنسبة إلى if err != nil { return nil, nil, err } (وهو أمر شائع جدًا) ، فإن التحقق من الخطأ يكون موجزًا ​​بقدر ما يمكن أن يكون دون التضحية بالوضوح (انظر الملاحظة على بناء الجملة أدناه).

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

جانب سلبي لأفكار مثل||وقبض علىأعلاه ، أو أ ، ب = فو ()؟ مقترح في موضوع آخر ، هو أنهم يخفون تعديل تدفق التحكم بطريقة تجعل متابعة التدفق أكثر صعوبة ؛ السابق مع ||يتم إلحاق الآلات في نهاية سطر عادي المظهر ، ويكون الأخير برمز صغير يمكن أن يظهر في كل مكان ، بما في ذلك في منتصف ونهاية سطر عادي المظهر من التعليمات البرمجية ، ربما عدة مرات.

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

هذه فكرة أخرى.

تخيل عبارة again التي تعرّف ماكرو بعلامة. يمكن توسيع العبارة التي تسميها مرة أخرى عن طريق الاستبدال النصي لاحقًا في الوظيفة (تذكرنا بـ const / iota ، مع ظلال goto: -]).

فمثلا:

func(foo int) (int, error) {
    err := f(foo)
again check:
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    check
    x, err := h()
    check
    return x, nil
}

سيكون معادلاً تمامًا لـ:

func(foo int) (int, error) {
    err := f(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    err = g(foo)
    if err != nil {
        return 0, errors.Wrap(err)
    }
    x, err := h()
    if err != nil {
        return 0, errors.Wrap(err)
    }
    return x, nil
}

لاحظ أن توسيع الماكرو لا يحتوي على أي حجج - وهذا يعني أنه يجب أن يكون هناك ارتباك أقل حول حقيقة أنه ماكرو ، لأن المحول البرمجي لا يحب الرموز من تلقاء نفسه .

مثل تعليمة goto ، يقع نطاق التسمية ضمن الوظيفة الحالية.

فكرة مشيقة. أعجبتني فكرة تسمية المصيد ، لكنني لا أعتقد أنها كانت مناسبة بشكل جيد مع نطاقات Go (مع Go الحالي ، لا يمكنك goto تسمية بمتغيرات جديدة محددة في نطاقها). تعمل فكرة again إصلاح هذه المشكلة لأن التسمية تأتي قبل طرح نطاقات جديدة.

ها هو المثال الضخم مرة أخرى:

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
    catch {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, try err = net.Listen("tcp4", listenAddr)

    conn, try err = ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    try err = toServer.Send(&client.serverConfig)

    try err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session, try err := communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

هذه نسخة أقرب إلى اقتراح روج (لا أحبها كثيرًا):

func NewClient(...) (*Client, error) {
    var (
        err      error
        listener net.Listener
        conn     net.Conn
    )
again:
    if err != nil {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener, err = net.Listen("tcp4", listenAddr)
    check

    conn, err = ConnectionManager{}.connect(server, tlsConfig)
    check

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    err = toServer.Send(&client.serverConfig)
    check

    err = toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})
    check

    session, err := communicationProtocol.FinalProtocol(conn)
    check
    client.session = session

    return client, nil
}

carlmjohnson للتسجيل ، هذا ليس تمامًا ما

أيضًا ، أود أن أقترح أن المثال أعلاه لا يوضح استخدامه جيدًا - لا يوجد شيء في العبارة المسمى مرة أخرى أعلاه لا يمكن القيام به بشكل جيد في بيان التأجيل. في مثال try / catch ، لا يمكن لهذا الرمز (على سبيل المثال) التفاف الخطأ بمعلومات حول موقع المصدر لإرجاع الخطأ. كما أنه لن يعمل AFAICS إذا أضفت "try" داخل إحدى عبارات if (على سبيل المثال للتحقق من الخطأ الذي أرجع بواسطة GetRuntimeEnvironment) ، لأن "الخطأ" المشار إليه في عبارة catch يقع في نطاق مختلف عن ذلك أعلن داخل الكتلة.

أعتقد أن مشكلتي الوحيدة مع الكلمة الرئيسية check هي أن جميع مرات الخروج من الوظيفة يجب أن تكون return (أو على الأقل تحتوي على دلالة "سأترك الوظيفة"). ربما نحصل على become (للتكلفة become لديه نوع من "أصبحنا وظيفة مختلفة" ... لكن كلمة "تحقق" لا تبدو مثل سيكون مخرجًا للوظيفة.

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

هل يمكنني إضافة اقتراح أيضا؟
ماذا عن شيء مثل هذا:

func Open(filename string) os.File onerror (string, error) {
       f, e := os.Open(filename)
       if e != nil { 
              fail "some reason", e // instead of return keyword to go on the onerror 
       }
      return f
}

f := Open(somefile) onerror reason, e {
      log.Prinln(reason)
      // try to recover from error and reasign 'f' on success
      nf = os.Create(somefile) onerror err {
             panic(err)
      }
      return nf // return here must return whatever Open returns
}

يمكن أن يكون لتعيين الخطأ أي شكل ، حتى لو كان شيئًا غبيًا مثله

f := Open(name) =: e

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

try {
    f := Open("file1") // can fail here
    defer f.Close()
    f1 := Open("file2") // can fail here
    defer f1.Close()
    // do something with the files
} onerror err {
     log.Println(err)
}

cthackers أنا شخصياً أعتقد أنه من الجيد جدًا وجود أخطاء في Go لعدم تلقي معاملة خاصة. إنها مجرد قيم ، وأعتقد أنها يجب أن تبقى على هذا النحو.

أيضًا ، try-catch (والتركيبات المماثلة) تدور حول بنية سيئة تشجع الممارسات السيئة. يجب معالجة كل خطأ بشكل منفصل ، وليس معالجته بواسطة معالج أخطاء "التقاط الكل".

https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md
هذا معقد للغاية.

فكرتي: |err| تعني تحقق من الخطأ: إذا لم يخطئ! = لا شيء {}

// common util func
func parseInt(s string) (i int64, err error){
    return strconv.ParseInt(s, 10, 64)
}

// expression check err 1 : check and use err variable
func parseAndSum(a string ,b string) (int64,error) {
    sum := parseInt(a) + parseInt(b)  |err| return 0,err
    return sum,nil
} 

// expression check err 2 : unuse variable 
func parseAndSum(a string , b string) (int64,error) {
    a,err := parseInt(a) |_| return 0, fmt.Errorf("parseInt error: %s", a)
    b,err := parseInt(b) |_| { println(b); return 0,fmt.Errorf("parseInt error: %s", b);}
    return a+b,nil
} 

// block check err 
func parseAndSum(a string , b string) (  int64,  error) {
    {
      a := parseInt(a)  
      b := parseInt(b)  
      return a+b,nil
    }|err| return 0,err
} 

@ chen56 وجميع المعلقين المستقبليين: راجع https://go.googlesource.com/proposal/+/master/design/go2draft.md .

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

المثال الضخم باستخدام اقتراح Go 2:

func NewClient(...) (*Client, error) {
    var (
        listener net.Listener
        conn     net.Conn
    )
    handle err {
        if conn != nil {
            conn.Close()
        }
        if listener != nil {
            listener.Close()
        }
        return nil, err
    }

    listener = check net.Listen("tcp4", listenAddr)

    conn = check ConnectionManager{}.connect(server, tlsConfig)

    if forwardPort == 0 {
        env, err := environment.GetRuntimeEnvironment()
        if err != nil {
            log.Printf("not forwarding because: %v", err)
        } else {
            forwardPort, err = env.PortToForward()
            if err != nil {
                log.Printf("env couldn't provide forward port: %v", err)
            }
        }
    }
    var forwardOut *forwarding.ForwardOut
    if forwardPort != 0 {
        u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", forwardPort))
        forwardOut = forwarding.NewOut(u)
    }

    client := &Client{...}

    toServer := communicationProtocol.Wrap(conn)
    check toServer.Send(&client.serverConfig)

    check toServer.Send(&stprotocol.ClientProtocolAck{ClientVersion: Version})

    session := check communicationProtocol.FinalProtocol(conn)
    client.session = session

    return client, nil
}

أعتقد أن هذا نظيف بقدر ما يمكن أن نأمله. تحتوي الكتلة handle على الصفات الجيدة لتسمية again أو الكلمة الأساسية Ruby's rescue . الأسئلة الوحيدة المتبقية في ذهني هي ما إذا كان يجب استخدام علامات الترقيم أو كلمة رئيسية (أعتقد كلمة رئيسية) وما إذا كنت تسمح بالحصول على الخطأ دون إعادته.

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

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

sdwarwick لا أعتقد أن هذا هو أفضل مكان لمناقشة مسودة التصميم الموضحة على https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md . تتمثل الطريقة الأفضل في إضافة رابط إلى كتابة على صفحة wiki على https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback .

ومع ذلك ، تسمح مسودة التصميم هذه بكتل مقبض متعددة في وظيفة ما.

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

إذا تحدث في مجموعة هذه الأمثلة:

r, err := os.Open(src)
    if err != nil {
        return err
    }

أود أن أكتب في سطر واحد تقريبًا:

r, err := os.Open(src) try ("blah-blah: %v", err)

بدلًا من "جرب" ضع أي كلمة جميلة ومناسبة.

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

حتى بعد قليل (بدون إضافة نوع من معالجة الأخطاء):

r, err := os.Open(src) try

)
ملاحظة: عفواً على لغتي الإنجليزية))

البديل الخاص بي:

func CopyFile(src, dst string) string, error {
    r := check os.Open(src) // return nil, error
    defer r.Close()

    // if error: run 1 defer and retun error message
    w := check os.Create(dst) // return nil, error
    defer w.Close()

    // if error: run 2, 1 defer and retun error message
    if check io.Copy(w, r) // return nil, error

}

func StartCopyFile() error {
  res := check CopyFile("1.txt", "2.txt")

  return nil
}

func main() {
  err := StartCopyFile()
  if err!= nil{
    fmt.printLn(err)
  }
}

مرحبا،

لدي فكرة بسيطة ، تعتمد بشكل فضفاض على كيفية عمل معالجة الأخطاء في الغلاف ، تمامًا مثل الاقتراح الأولي. في الغلاف ، يتم الإبلاغ عن الأخطاء من خلال قيم الإرجاع التي لا تساوي الصفر. يتم تخزين القيمة المعادة لآخر أمر / استدعاء في $؟ في القشرة. بالإضافة إلى الاسم المتغير الذي قدمه المستخدم ، يمكننا تلقائيًا تخزين قيمة الخطأ لآخر مكالمة في متغير محدد مسبقًا ويمكن التحقق منه بواسطة بناء جملة محدد مسبقًا. لقد اخترت ؟ كصيغة للإشارة إلى أحدث قيمة خطأ ، تم إرجاعها من استدعاء دالة في النطاق الحالي. لقد اخترت ! كاختصار إذا؟ ! = لا شيء {}. الاختيار ل؟ يتأثر بالصدفة ، ولكن أيضًا لأنه يبدو منطقيًا. إذا حدث خطأ ، فأنت بطبيعة الحال مهتم بما حدث. هذا يثير سؤالا. ؟ هي العلامة الشائعة لسؤال مطروح ، وبالتالي نستخدمها للإشارة إلى أحدث قيمة خطأ تم إنشاؤها في نفس النطاق.
! يستخدم كاختصار إذا؟ ! = لا شيء ، لأنه يشير إلى وجوب الانتباه في حالة حدوث خطأ ما. ! يعني: إذا حدث خطأ ما ، افعل هذا. ؟ تشير إلى أحدث قيمة خطأ. كالعادة قيمة؟ يساوي الصفر إذا لم يكن هناك خطأ.

val, err := someFunc(param)
! { return &SpecialError("someFunc", param, ?) }

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

سيسمح بهذا:

val, _ := someFunc(param)
! return &SpecialError("someFunc", param, ?)

هذا من شأنه أن يسمح

val, _ := someFunc(param) ! return &SpecialError("someFunc", param, ?)

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

val, _ := someFunc(param)
! {
val, _ := someFunc(paramAlternative)
  !{ return &SpecialError("someFunc alternative try failed too", paramAlternative, ?) }}

بموجب هذا الاقتراح يمكنك استخدام! في حلقة for لمحاولات متعددة مثل هذا.

val, _ := someFunc(param)
for i :=0; ! && i <5; i++ {
  // Sleep or make a change or both
  val, _ := someFunc(param)
} ! { return &SpecialError("someFunc", param, ? }

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

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

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

tobimensch ، يرجى نشر اقتراحات جديدة إلى جوهر ما ، وربط ذلك في ويكي ملاحظات معالجة أخطاء Go 2 . قد يتم التغاضي عن المشاركات في هذه القضية المغلقة.

إذا لم تكن قد شاهدته ، فقد ترغب في قراءة مسودة تصميم معالجة الأخطاء في

وقد تكون مهتمًا بالمتطلبات التي يجب مراعاتها في معالجة أخطاء Go 2 .

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

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات