Go الفعّالة › الدوال 7 / 16 السابق التالي

الدوال

عوائد متعددة

من الميزات غير المعتادة في Go أن الدوال والطرق تستطيع أن تُرجع عوائد متعددة. ويمكن استعمال هذه الصيغة لتحسين بعض الأعراف المتعثّرة في برامج C: مثل إرجاع الخطأ داخل قيمة الإرجاع نفسها، كإرجاع -1 للدلالة على EOF، أو تعديل وسيط مُمرَّر بالعنوان.

في C، يُشير خطأ الكتابة بعدد سالب مع إخفاء رمز الخطأ في موضع متغيّر. أمّا في Go، تستطيع Write أن تُرجع عددًا وخطأً معًا: «نعم، لقد كتبت بعض البايتات لكن ليس كلّها لأنك ملأت الجهاز». وتوقيع طريقة Write على الملفات من الحزمة os هو:

func (file *File) Write(b []byte) (n int, err error)

وكما يقول التوثيق، فإنها تُرجع عدد البايتات المكتوبة، وerror غير nil عندما يكون n != len(b). وهذا أسلوب شائع؛ انظر قسم معالجة الأخطاء لمزيد من الأمثلة.

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

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

ويمكنك استعمالها لمسح الأعداد في شريحة إدخال b على النحو الآتي:

for i := 0; i < len(b); {
    x, i = nextInt(b, i)
    fmt.Println(x)
}

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

معاملات النتائج المسمّاة

يمكن تسمية معاملات «الإرجاع» أو «النتائج» في دالة Go واستعمالها كمتغيّرات عادية، تمامًا كمعاملات الإدخال. وعندما تُسمّى، فإنها تُهيَّأ على القيم الصفرية لأنواعها عند بدء الدالة؛ وإذا نفّذت الدالة جملة return بلا وسائط، فإن القيم الحالية لمعاملات النتائج هي ما يُعاد.

الأسماء ليست إلزامية، لكنها تجعل الشيفرة أقصر وأوضح: فهي توثيق. فإذا سمّينا نتائج nextInt، صار واضحًا أيّ int مُعادة هي أيّها:

func nextInt(b []byte, pos int) (value, nextPos int) {

ولأن النتائج المسمّاة تُهيَّأ وترتبط بعودية مجرّدة، فإنها تُبسّط الشيفرة وتوضّحها في آن واحد. إليك نسخة من io.ReadFull تستعملها استعمالًا جيدًا:

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

defer

تجعل جملة defer في Go جدولة نداء دالة — الدالة المؤجَّلة — بحيث يُنفَّذ فورًا قبل أن تعود الدالة التي نفّذت defer. وهذه طريقة غير معتادة لكنها فعّالة في التعامل مع مواقف مثل الموارد التي يجب تحريرها أيًّا كان المسار الذي تعود منه الدالة. والأمثلة النموذجية هي فتح قفل (mutex) أو إغلاق ملف.

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

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

أما وسائط الدالة المؤجَّلة — وهي تشمل المُستقبِل إن كانت الدالة طريقة — فتُقوَّم لحظة تنفيذ defer، لا لحظة تنفيذ النداء. وبالإضافة إلى أنه يتفادى القلق من تغيّر قيم المتغيّرات أثناء تنفيذ الدالة، فإن هذا يعني أن موقع تأجيل واحد يمكنه تأجيل عدّة عمليات تنفيذ. إليك مثالًا ساذجًا:

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

وتُنفَّذ الدوال المؤجَّلة بترتيب LIFO، أي أن آخر داخل أول خارج، فيطبع هذا الشيفرة 4 3 2 1 0 عند عودة الدالة. ومثال أكثر واقعية هو طريقة بسيطة لتتبّع تنفيذ الدوال عبر البرنامج. يمكننا كتابة دالّتَي تتبّع بسيطتين كالتاليتين:

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

ويمكننا أن نفعل أفضل من ذلك باستغلال الحقيقة أن وسائط الدوال المؤجَّلة تُقوَّم لحظة تنفيذ defer. فيستطيع دالّة التتبّع أن تهيّئ وسيل دالّة إلغاء التتبّع. المثال الآتي:

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

يُطبع

entering: b
in b
entering: a
in a
leaving: a
leaving: b

وللمبرمجين المعتادين على إدارة الموارد على مستوى الكتلة في لغات أخرى، قد يبدو defer غريبًا، لكن أكثر تطبيقاته إثارةً وقوة تنبع تحديدًا من كونه غير قائم على الكتل بل على الدالة. وفي قسم panic وrecover سنرى مثالًا آخر لإمكاناته.